From ef93bbeb9bcca29eabe21492978aad5bf60ce640 Mon Sep 17 00:00:00 2001 From: Saksham Arya Date: Wed, 20 Nov 2024 16:02:55 +0530 Subject: [PATCH 1/5] make headers thread safe add reader lock --- context.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/context.go b/context.go index 842ad2ff..3fbf4af2 100644 --- a/context.go +++ b/context.go @@ -22,6 +22,7 @@ import ( "time" "github.com/gin-contrib/sse" + "github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/render" ) @@ -72,6 +73,9 @@ type Context struct { // This mutex protects Keys map. mu sync.RWMutex + // This mutex protects headers map + hmu sync.RWMutex + // Keys is a key/value pair exclusively for the context of each request. Keys map[any]any @@ -975,6 +979,8 @@ func (c *Context) IsWebsocket() bool { } func (c *Context) requestHeader(key string) string { + c.hmu.Lock() + defer c.hmu.Unlock() return c.Request.Header.Get(key) } @@ -1004,6 +1010,8 @@ func (c *Context) Status(code int) { // It writes a header in the response. // If value == "", this method removes the header `c.Writer.Header().Del(key)` func (c *Context) Header(key, value string) { + c.hmu.Lock() + defer c.hmu.Unlock() if value == "" { c.Writer.Header().Del(key) return From c7c51af0ec4ba7e41c97be0211a6e1227721f36f Mon Sep 17 00:00:00 2001 From: Saksham Arya Date: Wed, 20 Nov 2024 16:18:37 +0530 Subject: [PATCH 2/5] add test case fix lint --- context_test.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/context_test.go b/context_test.go index f51c147f..5cf56718 100644 --- a/context_test.go +++ b/context_test.go @@ -3422,6 +3422,24 @@ func TestContextSetCookieData(t *testing.T) { setCookie := c.Writer.Header().Get("Set-Cookie") assert.Contains(t, setCookie, "SameSite=None") }) +func TestParallelHeaderWrite(t *testing.T) { + c, _ := CreateTestContext(httptest.NewRecorder()) + wg := sync.WaitGroup{} + wg.Add(1) + go func() { + defer wg.Done() + for i := 0; i < 1000; i++ { + c.Header("key", "value") + } + }() + wg.Add(1) + go func() { + defer wg.Done() + for i := 0; i < 1000; i++ { + c.Header("key", "value") + } + }() + wg.Wait() } func TestGetMapFromFormData(t *testing.T) { From cdaceb18a5c6ea43e88438de8f1bfad034b84408 Mon Sep 17 00:00:00 2001 From: Saksham Arya Date: Thu, 26 Jun 2025 17:04:14 +0530 Subject: [PATCH 3/5] comment --- context.go | 4 ++-- context_test.go | 9 ++++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/context.go b/context.go index 3fbf4af2..d5b1cd27 100644 --- a/context.go +++ b/context.go @@ -979,8 +979,8 @@ func (c *Context) IsWebsocket() bool { } func (c *Context) requestHeader(key string) string { - c.hmu.Lock() - defer c.hmu.Unlock() + c.hmu.RLock() + defer c.hmu.RUnlock() return c.Request.Header.Get(key) } diff --git a/context_test.go b/context_test.go index 5cf56718..069ec195 100644 --- a/context_test.go +++ b/context_test.go @@ -27,12 +27,13 @@ import ( "time" "github.com/gin-contrib/sse" - "github.com/gin-gonic/gin/binding" - "github.com/gin-gonic/gin/codec/json" - testdata "github.com/gin-gonic/gin/testdata/protoexample" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/protobuf/proto" + + "github.com/gin-gonic/gin/binding" + "github.com/gin-gonic/gin/codec/json" + testdata "github.com/gin-gonic/gin/testdata/protoexample" ) var _ context.Context = (*Context)(nil) @@ -3422,6 +3423,8 @@ func TestContextSetCookieData(t *testing.T) { setCookie := c.Writer.Header().Get("Set-Cookie") assert.Contains(t, setCookie, "SameSite=None") }) +} + func TestParallelHeaderWrite(t *testing.T) { c, _ := CreateTestContext(httptest.NewRecorder()) wg := sync.WaitGroup{} From 312b621908c7056f9f8db0248b6c693ca39a7095 Mon Sep 17 00:00:00 2001 From: Saksham Arya Date: Fri, 27 Jun 2025 13:24:40 +0530 Subject: [PATCH 4/5] refactor test case --- context_test.go | 58 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 40 insertions(+), 18 deletions(-) diff --git a/context_test.go b/context_test.go index 069ec195..23069b6c 100644 --- a/context_test.go +++ b/context_test.go @@ -3425,24 +3425,46 @@ func TestContextSetCookieData(t *testing.T) { }) } -func TestParallelHeaderWrite(t *testing.T) { - c, _ := CreateTestContext(httptest.NewRecorder()) - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - defer wg.Done() - for i := 0; i < 1000; i++ { - c.Header("key", "value") - } - }() - wg.Add(1) - go func() { - defer wg.Done() - for i := 0; i < 1000; i++ { - c.Header("key", "value") - } - }() - wg.Wait() +func TestParallelHeaderAccess(t *testing.T) { + t.Parallel() + const iterations = 1000 + const goroutines = 8 + + testCases := []struct { + name string + writerCount int + readerCount int + }{ + {"parallel_write_only", goroutines, 0}, + {"parallel_write_and_read", goroutines / 2, goroutines / 2}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + c, _ := CreateTestContext(httptest.NewRecorder()) + c.Request, _ = http.NewRequest(http.MethodGet, "/", nil) + wg := sync.WaitGroup{} + for i := 0; i < tc.writerCount; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for _ = range iterations { + c.Header("key", "value") + } + }() + } + for i := 0; i < tc.readerCount; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for _ = range iterations { + _ = c.GetHeader("key") + } + }() + } + wg.Wait() + }) + } } func TestGetMapFromFormData(t *testing.T) { From 14fd129b90213b0260772169108d6526120c4ee2 Mon Sep 17 00:00:00 2001 From: Saksham Arya Date: Mon, 30 Jun 2025 15:25:44 +0530 Subject: [PATCH 5/5] fix lint --- context.go | 1 - context_test.go | 11 +++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/context.go b/context.go index d5b1cd27..66478c09 100644 --- a/context.go +++ b/context.go @@ -22,7 +22,6 @@ import ( "time" "github.com/gin-contrib/sse" - "github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/render" ) diff --git a/context_test.go b/context_test.go index 23069b6c..4775b218 100644 --- a/context_test.go +++ b/context_test.go @@ -27,13 +27,12 @@ import ( "time" "github.com/gin-contrib/sse" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "google.golang.org/protobuf/proto" - "github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/codec/json" testdata "github.com/gin-gonic/gin/testdata/protoexample" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" ) var _ context.Context = (*Context)(nil) @@ -3448,7 +3447,7 @@ func TestParallelHeaderAccess(t *testing.T) { wg.Add(1) go func() { defer wg.Done() - for _ = range iterations { + for range iterations { c.Header("key", "value") } }() @@ -3457,7 +3456,7 @@ func TestParallelHeaderAccess(t *testing.T) { wg.Add(1) go func() { defer wg.Done() - for _ = range iterations { + for range iterations { _ = c.GetHeader("key") } }()