diff --git a/context.go b/context.go index 1c76c0f6..dc9d99c5 100644 --- a/context.go +++ b/context.go @@ -1027,6 +1027,17 @@ func (c *Context) SetCookie(name, value string, maxAge int, path, domain string, }) } +// SetCookieStruct adds a Set-Cookie header to the ResponseWriter's headers. +// It accepts a pointer to http.Cookie structure for more flexibility in setting cookie attributes. +// The provided cookie must have a valid Name. Invalid cookies may be silently dropped. +func (c *Context) SetCookieStruct(cookie *http.Cookie) { + if cookie.Path == "" { + cookie.Path = "/" + } + cookie.SameSite = c.sameSite + http.SetCookie(c.Writer, cookie) +} + // Cookie returns the named cookie provided in the request or // ErrNoCookie if not found. And return the named cookie is unescaped. // If multiple cookies match the given name, only one cookie will diff --git a/context_test.go b/context_test.go index ef0cfccd..732a8199 100644 --- a/context_test.go +++ b/context_test.go @@ -3123,3 +3123,76 @@ func TestContextNext(t *testing.T) { assert.True(t, exists) assert.Equal(t, "value3", value) } + +func TestContextSetCookieStruct(t *testing.T) { + c, _ := CreateTestContext(httptest.NewRecorder()) + c.SetSameSite(http.SameSiteLaxMode) + + // Basic cookie settings + cookie := &http.Cookie{ + Name: "user", + Value: "gin", + MaxAge: 1, + Path: "/", + Domain: "localhost", + Secure: true, + HttpOnly: true, + } + c.SetCookieStruct(cookie) + assert.Equal(t, "user=gin; Path=/; Domain=localhost; Max-Age=1; HttpOnly; Secure; SameSite=Lax", c.Writer.Header().Get("Set-Cookie")) + + // Test that when Path is empty, "/" is automatically set + cookie = &http.Cookie{ + Name: "user", + Value: "gin", + MaxAge: 1, + Path: "", + Domain: "localhost", + Secure: true, + HttpOnly: true, + } + c.SetCookieStruct(cookie) + assert.Equal(t, "user=gin; Path=/; Domain=localhost; Max-Age=1; HttpOnly; Secure; SameSite=Lax", c.Writer.Header().Get("Set-Cookie")) + + // Test additional cookie attributes (Expires) + expireTime := time.Now().Add(24 * time.Hour) + cookie = &http.Cookie{ + Name: "user", + Value: "gin", + Path: "/", + Domain: "localhost", + Expires: expireTime, + Secure: true, + HttpOnly: true, + } + c.SetCookieStruct(cookie) + + // Since the Expires value varies by time, partially verify with Contains + setCookie := c.Writer.Header().Get("Set-Cookie") + assert.Contains(t, setCookie, "user=gin") + assert.Contains(t, setCookie, "Path=/") + assert.Contains(t, setCookie, "Domain=localhost") + assert.Contains(t, setCookie, "HttpOnly") + assert.Contains(t, setCookie, "Secure") + assert.Contains(t, setCookie, "SameSite=Lax") + + // Test for Partitioned attribute (Go 1.18+) + cookie = &http.Cookie{ + Name: "user", + Value: "gin", + Path: "/", + Domain: "localhost", + Secure: true, + HttpOnly: true, + Partitioned: true, + } + c.SetCookieStruct(cookie) + setCookie = c.Writer.Header().Get("Set-Cookie") + assert.Contains(t, setCookie, "user=gin") + assert.Contains(t, setCookie, "Path=/") + assert.Contains(t, setCookie, "Domain=localhost") + assert.Contains(t, setCookie, "HttpOnly") + assert.Contains(t, setCookie, "Secure") + assert.Contains(t, setCookie, "SameSite=Lax") + // Not testing for Partitioned attribute as it may not be supported in all Go versions +} diff --git a/docs/doc.md b/docs/doc.md index 9b7b1ec9..d05d3a87 100644 --- a/docs/doc.md +++ b/docs/doc.md @@ -2319,6 +2319,47 @@ func main() { } ``` +You can also use the `SetCookieStruct` method, which accepts a `*http.Cookie` directly for more flexibility: + +```go +import ( + "fmt" + "net/http" + "time" + + "github.com/gin-gonic/gin" +) + +func main() { + router := gin.Default() + + router.GET("/cookie", func(c *gin.Context) { + cookie, err := c.Cookie("gin_cookie") + + if err != nil { + cookie = "NotSet" + // Using http.Cookie struct for more control + c.SetCookieStruct(&http.Cookie{ + Name: "gin_cookie", + Value: "test", + Path: "/", + Domain: "localhost", + MaxAge: 3600, + Secure: false, + HttpOnly: true, + // Additional fields available in http.Cookie + Expires: time.Now().Add(24 * time.Hour), + // Partitioned: true, // Available in newer Go versions + }) + } + + fmt.Printf("Cookie value: %s \n", cookie) + }) + + router.Run() +} +``` + ## Don't trust all proxies Gin lets you specify which headers to hold the real client IP (if any),