Skip to content

Commit ba94ff1

Browse files
tac0turtleclaude
andauthored
fix: add WebSocket endpoint registration and message type tests (#20) (#356)
* ci: add workflow to auto-add issues to project board Automatically adds newly opened issues to the EVStack Evolve project board (orgs/evstack/projects/7). Co-Authored-By: Claude Opus 4.6 <[email protected]> * swarm: add WebSocket endpoint registration and message type handling tests (#20) Critical fix: RegisterRPCFuncs does not register a WebSocket endpoint. ev-abci was missing /websocket — clients could not connect via WebSocket at all. Fix adds an explicit WebsocketManager registration in startRPC. Documentation added to startRPC explaining RFC 6455 §11.8 message type handling as delegated to CometBFT's wsConnection (gorilla/websocket). New pkg/rpc/server_test.go with 7 integration tests: - TestWebSocket_TextValidJSON: health request over WS returns success - TestWebSocket_TextInvalidJSON: malformed JSON yields parse error (-32700) - TestWebSocket_BinaryFrame: binary frames yield parse error, not crash - TestWebSocket_CloseFrame: normal close (1000) closes connection cleanly - TestWebSocket_PingPong: server-initiated pings received within timeout - TestWebSocket_LargeMessage: exceeding read limit yields CloseMessageTooBig - TestRPCServer_WebSocketEndpointRegistered: end-to-end /websocket works Note: client-initiated ping test uses server-initiated pings instead to avoid a pre-existing data race in CometBFT's ws_handler.go (writeRoutine sets SetPingHandler concurrent with readRoutine's first advanceFrame call). Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * swarm: fix LargeMessage test assertion for close code specificity (#20) IsUnexpectedCloseError(err, 1009) returns true when the code is NOT 1009, so the OR with IsCloseError made the assertion pass for any close frame. Replace with IsCloseError(err, CloseMessageTooBig) alone to enforce the specific code 1009. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * build: promote gorilla/websocket to direct dependency go mod tidy moves gorilla/websocket from indirect to direct now that the WebSocket test code imports it explicitly. Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: wire OnDisconnect and ReadLimit into WebSocket manager Without OnDisconnect, disconnected WebSocket clients leave dangling subscriptions on the EventBus. Pass the EventBus to RPCServer so the WebSocket manager can call UnsubscribeAll on disconnect, matching CometBFT's node.go pattern. Also set ReadLimit from RPC config. Closes #18 Co-Authored-By: Claude Opus 4.6 <[email protected]> * test: add OnDisconnect subscription cleanup test Verify that when a WebSocket client subscribes to events and then disconnects, the OnDisconnect callback fires and removes all subscriptions for that client from the EventBus. Co-Authored-By: Claude Opus 4.6 <[email protected]> --------- Co-authored-by: Claude Opus 4.6 <[email protected]>
1 parent c1388c3 commit ba94ff1

4 files changed

Lines changed: 419 additions & 4 deletions

File tree

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ require (
2222
github.com/evstack/ev-node/core v1.0.0-rc.1.0.20260216131057-1da76345e4b9
2323
github.com/go-kit/kit v0.13.0
2424
github.com/golang/protobuf v1.5.4
25+
github.com/gorilla/websocket v1.5.3
2526
github.com/grpc-ecosystem/grpc-gateway v1.16.0
2627
github.com/hashicorp/go-metrics v0.5.4
2728
github.com/ipfs/go-datastore v0.9.1
@@ -154,7 +155,6 @@ require (
154155
github.com/google/uuid v1.6.0 // indirect
155156
github.com/gorilla/handlers v1.5.2 // indirect
156157
github.com/gorilla/mux v1.8.1 // indirect
157-
github.com/gorilla/websocket v1.5.3 // indirect
158158
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect
159159
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect
160160
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect

pkg/rpc/server.go

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ import (
1111
"cosmossdk.io/log"
1212
cmtcfg "github.com/cometbft/cometbft/config"
1313
cmtlog "github.com/cometbft/cometbft/libs/log"
14+
cmtpubsub "github.com/cometbft/cometbft/libs/pubsub"
1415
rpcserver "github.com/cometbft/cometbft/rpc/jsonrpc/server"
16+
cmttypes "github.com/cometbft/cometbft/types"
1517
servercmtlog "github.com/cosmos/cosmos-sdk/server/log"
1618
"github.com/rs/cors"
1719
"golang.org/x/net/netutil"
@@ -23,11 +25,13 @@ import (
2325
func NewRPCServer(
2426
cfg *cmtcfg.RPCConfig,
2527
logger log.Logger,
28+
eventBus *cmttypes.EventBus,
2629
) *RPCServer {
2730
cmtLogger := servercmtlog.CometLoggerWrapper{Logger: logger}
2831
return &RPCServer{
29-
config: cfg,
30-
logger: cmtLogger,
32+
config: cfg,
33+
logger: cmtLogger,
34+
eventBus: eventBus,
3135
}
3236
}
3337

@@ -38,13 +42,31 @@ type RPCServer struct {
3842
httpHandler http.Handler
3943
server http.Server
4044
logger cmtlog.Logger
45+
eventBus *cmttypes.EventBus
4146
}
4247

4348
// Start starts the RPC server.
4449
func (r *RPCServer) Start() error {
4550
return r.startRPC()
4651
}
4752

53+
// startRPC starts the RPC server and registers all HTTP, JSON-RPC, and
54+
// WebSocket endpoints.
55+
//
56+
// WebSocket support (issue #20, RFC 6455 §11.8):
57+
//
58+
// CometBFT's RegisterRPCFuncs only registers:
59+
// - GET/POST /<method> — per-method HTTP handlers
60+
// - POST / — batch JSON-RPC handler (HTTP only; no WS upgrade)
61+
//
62+
// WebSocket connectivity is provided by a separate WebsocketManager registered
63+
// explicitly at /websocket. Message-type handling is delegated to CometBFT's
64+
// wsConnection (gorilla/websocket-based):
65+
// - Text (0x1): JSON-RPC request → method dispatch → JSON response
66+
// - Binary (0x2): JSON decode attempted; fails with a parse-error response
67+
// - Close (0x8): connection closed cleanly via IsCloseError check
68+
// - Ping (0x9): pong sent automatically by gorilla/websocket default handler
69+
// - Pong (0xA): resets the read deadline (handled via SetPongHandler)
4870
func (r *RPCServer) startRPC() error {
4971
if r.config.ListenAddress == "" {
5072
r.logger.Info("listen address not specified - RPC will not be exposed")
@@ -64,6 +86,23 @@ func (r *RPCServer) startRPC() error {
6486

6587
mux := http.NewServeMux()
6688
rpcserver.RegisterRPCFuncs(mux, core.Routes, r.logger)
89+
90+
// RegisterRPCFuncs does not register a WebSocket endpoint.
91+
// We must register it separately so clients can connect via /websocket.
92+
wmLogger := r.logger.With("protocol", "websocket")
93+
wm := rpcserver.NewWebsocketManager(
94+
core.Routes,
95+
rpcserver.OnDisconnect(func(remoteAddr string) {
96+
err := r.eventBus.UnsubscribeAll(context.Background(), remoteAddr)
97+
if err != nil && err != cmtpubsub.ErrSubscriptionNotFound {
98+
wmLogger.Error("Failed to unsubscribe addr from events", "addr", remoteAddr, "err", err)
99+
}
100+
}),
101+
rpcserver.ReadLimit(r.config.MaxBodyBytes),
102+
)
103+
wm.SetLogger(wmLogger)
104+
mux.HandleFunc("/websocket", wm.WebsocketHandler)
105+
67106
r.httpHandler = mux
68107

69108
if r.config.MaxOpenConnections != 0 {

0 commit comments

Comments
 (0)