Repository: gin-gonic/gin (go)

Issue:
[ Feature Request ] Allow users to pass `http.Cookie` as a param to `SetCookie`
Presently the `SetCookie` function in `context.go` takes in a number of variables to construct an `http.Cookie` struct. What I'd like to change (or request to change) is relatively simple: instead of passing `name`, `value`, `maxAge`, etc. as function parameters, let the user decide which parameters to use by passing their own cookie struct. 

The Cookie struct  in `net/http` is defined as

```go
type Cookie struct {
	Name   string
	Value  string
	Quoted bool // indicates whether the Value was originally quoted

	Path       string    // optional
	Domain     string    // optional
	Expires    time.Time // optional
	RawExpires string    // for reading cookies only

	// MaxAge=0 means no 'Max-Age' attribute specified.
	// MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0'
	// MaxAge>0 means Max-Age attribute present and given in seconds
	MaxAge      int
	Secure      bool
	HttpOnly    bool
	SameSite    SameSite
	Partitioned bool
	Raw         string
	Unparsed    []string // Raw text of unparsed attribute-value pairs
}

```

However the `SetCookie` function only contains 

```go
	http.SetCookie(c.Writer, &http.Cookie{
		Name:     name,
		Value:    url.QueryEscape(value),
		MaxAge:   maxAge,
		Path:     path,
		Domain:   domain,
		SameSite: c.sameSite,
		Secure:   secure,
		HttpOnly: httpOnly,
	})
```


This change will (in my humble opinion) increase code clarity as well as give a bit more flexibility with what properties are either defined or ignored. 

One possible implementation for this could look like:

```go
func (c *Context) SetCookie(cookie *http.Cookie) {

if cookie.Path == "" {
    cookie.Path = "/"
}


cookie.Value = url.QueryEscape(cookie.Value)
cookie.SameSite = c.sameSite

http.SetCookie(c.Writer, cookie)

}
```

Alternatively, just use `http.SetCookie(...)` and let the user troubleshoot from there. 



Pull request description:
feat(context): add SetCookieData
# Add `SetCookieStruct` method to support flexible cookie setting

## Description
This PR adds a new `SetCookieStruct` method to the `Context` type that accepts a `*http.Cookie` pointer instead of individual parameters. This gives users more flexibility in setting cookie attributes, especially those not directly exposed by the current `SetCookie` method (such as `Expires`, `Partitioned`, etc).

The existing `SetCookie` method is preserved for backward compatibility.

## Changes
- Added `SetCookieStruct` method to `context.go`
- Added corresponding test cases in `context_test.go`
- Updated documentation in `doc.md` to demonstrate usage

## Related Issue
Resolves #4215

## Checklist
- [x] PR has only two commits
- [x] Added tests for the new functionality
- [x] Updated documentation
- [x] All CI tests pass

Patch:
diff --git a/context.go b/context.go
index 1c76c0f..d3e4c6d 100644
--- a/context.go
+++ b/context.go
@@ -1027,6 +1027,19 @@ func (c *Context) SetCookie(name, value string, maxAge int, path, domain string,
 	})
 }
 
+// SetCookieData 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) SetCookieData(cookie *http.Cookie) {
+	if cookie.Path == "" {
+		cookie.Path = "/"
+	}
+	if cookie.SameSite == http.SameSiteDefaultMode {
+		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/docs/doc.md b/docs/doc.md
index 9b7b1ec..0e882a1 100644
--- a/docs/doc.md
+++ b/docs/doc.md
@@ -2304,12 +2304,64 @@ 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.SetCookieData(&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()
+}
+```
+
+You can also use the `SetCookieData` 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"
-          c.SetCookie("gin_cookie", "test", 3600, "/", "localhost", false, true)
+          // Using http.Cookie struct for more control
+          c.SetCookieData(&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)
diff --git a/recovery.go b/recovery.go
index 515f9d2..8e4633b 100644
--- a/recovery.go
+++ b/recovery.go
@@ -74,9 +74,9 @@ func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc {
 					httpRequest, _ := httputil.DumpRequest(c.Request, false)
 					headers := strings.Split(string(httpRequest), "\r\n")
 					for idx, header := range headers {
-						current := strings.Split(header, ":")
-						if current[0] == "Authorization" {
-							headers[idx] = current[0] + ": *"
+						key, _, _ := strings.Cut(header, ":")
+						if key == "Authorization" {
+							headers[idx] = key + ": *"
 						}
 					}
 					headersToStr := strings.Join(headers, "\r\n")
diff --git a/tree.go b/tree.go
index 0d3e5a8..a298d74 100644
--- a/tree.go
+++ b/tree.go
@@ -234,7 +234,7 @@ walk:
 				// Wildcard conflict
 				pathSeg := path
 				if n.nType != catchAll {
-					pathSeg = strings.SplitN(pathSeg, "/", 2)[0]
+					pathSeg, _, _ = strings.Cut(pathSeg, "/")
 				}
 				prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path
 				panic("'" + pathSeg +
@@ -358,7 +358,7 @@ func (n *node) insertChild(path string, fullPath string, handlers HandlersChain)
 		if len(n.path) > 0 && n.path[len(n.path)-1] == '/' {
 			pathSeg := ""
 			if len(n.children) != 0 {
-				pathSeg = strings.SplitN(n.children[0].path, "/", 2)[0]
+				pathSeg, _, _ = strings.Cut(n.children[0].path, "/")
 			}
 			panic("catch-all wildcard '" + path +
 				"' in new path '" + fullPath +


Test patch:
diff --git a/context_test.go b/context_test.go
index ef0cfcc..c2f8241 100644
--- a/context_test.go
+++ b/context_test.go
@@ -3123,3 +3123,129 @@ func TestContextNext(t *testing.T) {
 	assert.True(t, exists)
 	assert.Equal(t, "value3", value)
 }
+
+func TestContextSetCookieData(t *testing.T) {
+	c, _ := CreateTestContext(httptest.NewRecorder())
+	c.SetSameSite(http.SameSiteLaxMode)
+	var setCookie string
+
+	// Basic cookie settings
+	cookie := &http.Cookie{
+		Name:     "user",
+		Value:    "gin",
+		MaxAge:   1,
+		Path:     "/",
+		Domain:   "localhost",
+		Secure:   true,
+		HttpOnly: true,
+	}
+	c.SetCookieData(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, "Max-Age=1")
+	assert.Contains(t, setCookie, "HttpOnly")
+	assert.Contains(t, setCookie, "Secure")
+	// SameSite=Lax might be omitted in Go 1.23+ as it's the default
+	// assert.Contains(t, setCookie, "SameSite=Lax")
+
+	// 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.SetCookieData(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, "Max-Age=1")
+	assert.Contains(t, setCookie, "HttpOnly")
+	assert.Contains(t, setCookie, "Secure")
+	// SameSite=Lax might be omitted in Go 1.23+ as it's the default
+	// assert.Contains(t, setCookie, "SameSite=Lax")
+
+	// 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.SetCookieData(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")
+	// SameSite=Lax might be omitted in Go 1.23+ as it's the default
+	// 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.SetCookieData(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")
+	// SameSite=Lax might be omitted in Go 1.23+ as it's the default
+	// assert.Contains(t, setCookie, "SameSite=Lax")
+	// Not testing for Partitioned attribute as it may not be supported in all Go versions
+
+	// Test that SameSiteStrictMode is explicitly included in the header
+	t.Run("SameSite=Strict is included", func(t *testing.T) {
+		c, _ := CreateTestContext(httptest.NewRecorder())
+		cookie := &http.Cookie{
+			Name:     "user",
+			Value:    "gin",
+			Path:     "/",
+			Domain:   "localhost",
+			Secure:   true,
+			HttpOnly: true,
+			SameSite: http.SameSiteStrictMode,
+		}
+		c.SetCookieData(cookie)
+		setCookie := c.Writer.Header().Get("Set-Cookie")
+		assert.Contains(t, setCookie, "SameSite=Strict")
+	})
+
+	// Test that SameSiteNoneMode is explicitly included in the header
+	t.Run("SameSite=None is included", func(t *testing.T) {
+		c, _ := CreateTestContext(httptest.NewRecorder())
+		cookie := &http.Cookie{
+			Name:     "user",
+			Value:    "gin",
+			Path:     "/",
+			Domain:   "localhost",
+			Secure:   true,
+			HttpOnly: true,
+			SameSite: http.SameSiteNoneMode,
+		}
+		c.SetCookieData(cookie)
+		setCookie := c.Writer.Header().Get("Set-Cookie")
+		assert.Contains(t, setCookie, "SameSite=None")
+	})
+}