Repository: gin-gonic/gin (Go)

Issue:
Version 1.11.0 does not work with github.com/coder/websocket.
### Description

Version 1.11.0 does not work anymore with github.com/coder/websocket, because it rejects writting headers before Hijack().

In github.com/coder/websocket a [fix](https://github.com/nhooyr/websocket/issues/166) has been made in 2019 to work with Gin, which calls `WriteHeaderNow()` before Hijack() in its `Accept()` function.
But `responseWriter.Hijack()` first tests for `w.Written()`, which returns true as size is 0, and returns an error (`errHijackAlreadyWritten`).

So, for maximum compatibility, and unless there is a good reason to block this, I think you should change
`if w.Written() {` to `if w.size>0 {`.


### Gin Version

1.11.0

### Can you reproduce the bug?

Yes

### Source Code

```
package main

import (
	"context"
	"log"
	"time"

	"github.com/coder/websocket"
	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()
	r.Any("/ws", func(c *gin.Context) {
		ws, err := websocket.Accept(c.Writer, c.Request, nil)
		if err != nil {
			// This is were we receive errHijackAlreadyWritten
			log.Print("Websocket upgrade failed: ", err)
			return
		}
		defer ws.Close(websocket.StatusNormalClosure, "")
	})

	go r.Run()

	// Let the server start...
	time.Sleep(time.Second)

	// Try to open the websocket
	ws, _, err := websocket.Dial(context.Background(), "ws://localhost:8080/ws", nil)
	if err != nil {
		log.Fatalf("FAILED to dial websocket: %v", err)
	}
	defer ws.Close(websocket.StatusGoingAway, "leaving")
}
```

### Go Version

1.25.1

### Operating System

UbuntU 24.04


Discussion:
See the PR: https://github.com/gin-gonic/gin/pull/4373



Patch:
diff --git a/response_writer.go b/response_writer.go
index ab2f5fec68..6907e51437 100644
--- a/response_writer.go
+++ b/response_writer.go
@@ -17,7 +17,7 @@ const (
 	defaultStatus = http.StatusOK
 )
 
-var errHijackAlreadyWritten = errors.New("gin: response already written")
+var errHijackAlreadyWritten = errors.New("gin: response body already written")
 
 // ResponseWriter ...
 type ResponseWriter interface {
@@ -109,7 +109,9 @@ func (w *responseWriter) Written() bool {
 
 // Hijack implements the http.Hijacker interface.
 func (w *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
-	if w.Written() {
+	// Allow hijacking before any data is written (size == -1) or after headers are written (size == 0),
+	// but not after body data is written (size > 0). For compatibility with websocket libraries (e.g., github.com/coder/websocket)
+	if w.size > 0 {
 		return nil, nil, errHijackAlreadyWritten
 	}
 	if w.size < 0 {


Test patch:
diff --git a/response_writer_test.go b/response_writer_test.go
index ef19841860..dfc1d2c60d 100644
--- a/response_writer_test.go
+++ b/response_writer_test.go
@@ -194,6 +194,64 @@ func TestResponseWriterHijackAfterWrite(t *testing.T) {
 	}
 }
 
+// Test: WebSocket compatibility - allow hijack after WriteHeaderNow(), but block after body data.
+func TestResponseWriterHijackAfterWriteHeaderNow(t *testing.T) {
+	tests := []struct {
+		name                      string
+		action                    func(w ResponseWriter) error
+		expectWrittenBeforeHijack bool
+		expectHijackSuccess       bool
+		expectWrittenAfterHijack  bool
+		expectError               error
+	}{
+		{
+			name: "hijack after WriteHeaderNow only should succeed (websocket pattern)",
+			action: func(w ResponseWriter) error {
+				w.WriteHeaderNow() // Simulate websocket.Accept() behavior
+				return nil
+			},
+			expectWrittenBeforeHijack: true,
+			expectHijackSuccess:       true, // NEW BEHAVIOR: allow hijack after just header write
+			expectWrittenAfterHijack:  true,
+			expectError:               nil,
+		},
+		{
+			name: "hijack after WriteHeaderNow + Write should fail",
+			action: func(w ResponseWriter) error {
+				w.WriteHeaderNow()
+				_, err := w.Write([]byte("test"))
+				return err
+			},
+			expectWrittenBeforeHijack: true,
+			expectHijackSuccess:       false,
+			expectWrittenAfterHijack:  true,
+			expectError:               errHijackAlreadyWritten,
+		},
+	}
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			hijacker := &mockHijacker{ResponseRecorder: httptest.NewRecorder()}
+			writer := &responseWriter{}
+			writer.reset(hijacker)
+			w := ResponseWriter(writer)
+
+			require.NoError(t, tc.action(w), "unexpected error during pre-hijack action")
+
+			assert.Equal(t, tc.expectWrittenBeforeHijack, w.Written(), "unexpected w.Written() state before hijack")
+
+			_, _, hijackErr := w.Hijack()
+
+			if tc.expectError == nil {
+				require.NoError(t, hijackErr, "expected hijack to succeed")
+			} else {
+				require.ErrorIs(t, hijackErr, tc.expectError, "unexpected error from Hijack()")
+			}
+			assert.Equal(t, tc.expectHijackSuccess, hijacker.hijacked, "unexpected hijacker.hijacked state")
+			assert.Equal(t, tc.expectWrittenAfterHijack, w.Written(), "unexpected w.Written() state after hijack")
+		})
+	}
+}
+
 func TestResponseWriterFlush(t *testing.T) {
 	testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		writer := &responseWriter{}