Repository: gofr-dev/gofr

Issue:
Error while concurrently writing to WebSocket using WriteMessageToSocket
**Describe the bug**

When multiple goroutines attempt to write data to a WebSocket simultaneously using `WriteMessageToSocket(wsMessage)`, it throws an error.  

**To Reproduce**
- Use multiple goroutines to write data to a WebSocket.  
- Call `gofrCtx.WriteMessageToSocket(wsMessage)` inside each goroutine.  
- Observe the error.  

1. The code is

```go
func test(gofrCtx *gofr.Context){
     err = gofrCtx.WriteMessageToSocket(wsMessage)
}   
```

2. The error is

```
panic: concurrent write to websocket connection

goroutine 434 [running]: github.com/gorilla/websocket.(*messageWriter).flushFrame(0x14000c93d28, 0x1, {0x140006c8953?, 0x1033e6634?, 0x1039f79e0?})
```

**Expected behavior**
Concurrent writes to a Web socket should not cause errors. The function should handle multiple goroutines writing data safely without requiring external synchronization.

**Environments (please complete the following information):**
- OS : MacOS
- gofr version : v1.35.0
- go version :  go1.24.1 darwin/arm64

**More description**
i solved the problem temporarily by using
```go
func test(gofrCtx *gofr.Context){
     mu.Lock()
     err = gofrCtx.WriteMessageToSocket(wsMessage)
     mu.Unlock()
}   
```


Hints from issue comments:
@kuldeep-bishnoi: can work on this, assign it to me. @Umang01-hash @Umang01-hash: sure @kuldeep-bishnoi done!@Ale-Cas: Hi @deadland2002, I'm not sure the assumptions here are correct, in particular WriteMessageToSocket seems to just wrap gorilla/websocket WriteMessage, which the documentation mentions explicitly as a not thread-safe method, see https://pkg.go.dev/github.com/gorilla/websocket#hdr-Concurrency

So, since this method is just a wrapper, it inherits the concurrency requirements from gorilla's implementation, and I think this should not be labeled as a bug, but as a possible improvement over the current implementation. 

Adding a mutex as you suggested could be an improvement, but could also lead to unexpected behavior in some cases. 
With a mutex, you wouldn't be sure which goroutine took the lock and the ordering of how messages are sent to the connection would be undefined. 
Instead, I'd suggest that you send all messages you want to write to the connection in a channel, then in just one goroutines you consume the channel and write the messages on the ws connection. This way you can control the order of the messages, if you need to.

This kind of pattern can also be exposed in another method of this library (to avoid breaking changes), while for this one it should be explicitly documented that only one concurrent call is supported.

Of course, nothing prevents this method from being thread-safe using a mutex, and if somebody has to preserv ordering that must be implemented on that specific application.

Btw, there are also discussions on gorilla/websocket about supporting thread-safe write methods https://github.com/gorilla/websocket/issues/826#issuecomment-2323481052
@Umang01-hash: @Ale-Cas thankyou for sharing this insight.
Will remove the label `bug` and add `improvement`@Umang01-hash: Hey @kuldeep-bishnoi.
Are you working on this enhancement? Please feel free to ask in case of any queries. @Ale-Cas: Hi @Umang01-hash I opened a PR that improves this method and also exposes the WriteJsonToSocket method which leverages the [underlying WriteJSON method in gorilla/websocket](https://github.com/gorilla/websocket/blob/e064f32e3674d9d79a8fd417b5bc06fa5c6cad8f/json.go#L23). 
These methods can be called concurrently from the ws connection wrapper of this library, while that can't be done when using the gorilla implementation.

Still, I had some issues with running the linters, it seems to me the current CI configuration might be broken on the development branch, cause of an [incompatibility between go v1.24 and golangci-lint v1.62](https://github.com/golangci/golangci-lint/issues/5596#issuecomment-2748052858). 
It could be just a matter of bumping the linter to v1.64, would you prefer to track this change with a separate issue / PR? @Umang01-hash: Sure @Ale-Cas. This PR #1607 upgrades linter version to `v1.64.0`.
And you can also check failed linters on this worklfow run: https://github.com/gofr-dev/gofr/actions/runs/14052279601/job/39344979248@Ale-Cas: Perfect, thanks @Umang01-hash, I saw it was merged now. I rebased but it's still not working: 
```sh
Error: can't load config: the Go language version (go1.23) used to build golangci-lint is lower than the targeted Go version (1.24)
Failed executing command with error: can't load config: the Go language version (go1.23) used to build golangci-lint is lower than the targeted Go version (1.24)
```
I think the problem is due to how the linter is installed in the CI:
`go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.64`
see https://github.com/golangci/golangci-lint/issues/5225
Instead of using `go install` the official binaries (compiled with go v1.24) should be used. 

I opened this issue -> https://github.com/gofr-dev/gofr/issues/1608

Solution patch:
diff --git a/docs/advanced-guide/websocket/page.md b/docs/advanced-guide/websocket/page.md
index 137eeeb8..4576a6be 100644
--- a/docs/advanced-guide/websocket/page.md
+++ b/docs/advanced-guide/websocket/page.md
@@ -51,6 +51,10 @@ GoFr allows us to customize the WebSocket upgrader with several options. We can
 - `CheckOrigin (WithCheckOrigin)`: Sets a custom origin check function.
 - `Compression (WithCompression)`:  Enables compression.
 
+## Writing Messages
+
+GoFr provides the `WriteMessageToSocket` method to send messages to the underlying websocket connection in a thread-safe way. The data parameter can be a string, []byte, or any struct that can be marshaled to JSON.
+
 ## Example:
 We can configure the Upgrader by creating a chain of option functions provided by GoFr.
 
diff --git a/pkg/gofr/websocket/websocket.go b/pkg/gofr/websocket/websocket.go
index 0f6c17f1..e91ef69a 100644
--- a/pkg/gofr/websocket/websocket.go
+++ b/pkg/gofr/websocket/websocket.go
@@ -19,6 +19,9 @@ const WSConnectionKey WSKey = "ws-connection-key"
 // Connection is a wrapper for gorilla websocket connection.
 type Connection struct {
 	*websocket.Conn
+
+	// Mutex to prevent race conditions on write operations
+	writeMutex sync.Mutex
 }
 
 // ErrorConnection is the connection error that occurs when webscoket connection cannot be established.
@@ -76,6 +79,16 @@ func (w *Connection) Bind(v any) error {
 	return nil
 }
 
+// WriteMessage writes the data on the underlying ws connection.
+//
+// This method is thread-safe and be called concurrently with WriteJSON.
+func (w *Connection) WriteMessage(messageType int, data []byte) error {
+	w.writeMutex.Lock()
+	defer w.writeMutex.Unlock()
+
+	return w.Conn.WriteMessage(messageType, data)
+}
+
 func (*Connection) HostName() string {
 	return "" // Not applicable for WebSocket, can be implemented if needed
 }



Test patch:
diff --git a/pkg/gofr/websocket/websocket_test.go b/pkg/gofr/websocket/websocket_test.go
index 602f2ce8..5e663365 100644
--- a/pkg/gofr/websocket/websocket_test.go
+++ b/pkg/gofr/websocket/websocket_test.go
@@ -5,6 +5,7 @@ import (
 	"net/http"
 	"net/http/httptest"
 	"os"
+	"sync"
 	"testing"
 	"time"
 
@@ -150,3 +151,40 @@ func dereference(v any) any {
 		return v
 	}
 }
+
+func TestConcurrentWriteMessageCalls(t *testing.T) {
+	upgrader := websocket.Upgrader{}
+
+	const message = "this is a test message"
+
+	loop := 10
+	workers := 10
+
+	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		conn, err := upgrader.Upgrade(w, r, nil)
+		assert.NoError(t, err)
+		defer conn.Close()
+
+		wc := &Connection{Conn: conn}
+
+		wg := sync.WaitGroup{}
+
+		for range loop {
+			for range workers {
+				wg.Add(1)
+
+				go func() {
+					defer wg.Done()
+
+					if err := wc.WriteMessage(websocket.TextMessage, []byte(message)); err != nil {
+						t.Errorf("concurrently wc.WriteMessage() returned %v", err)
+					}
+				}()
+			}
+		}
+
+		wg.Wait()
+	}))
+
+	server.Close()
+}