Skip to content

Commit f441387

Browse files
committed
Add websocket support to jsonrpc package
This commit only modifies the jsonrpc package. It doesn't provide a way to serve websocket connections when Juno is started, which can be implemented in a future PR. Two design decisions: We use the nhooyr.io/websocket library. The widely used gorilla/websocket library has been archived and geth is already running into issues [1]. The most notable consumer of nhooyr.io/websocket that I could find is Tailscale [2,3]. Unlike geth [4] and cometbft [5], we don't send ping messages to the client. Pro: saves bandwidth. Con: if the client app hangs, the Websocket connection will persist. As of 2017, Chromium is doing the same [6]. Note that the TCP keep-alives sent every 15 seconds [7] by the HTTP server will detect when the TCP connection is down, but will not detect when the application using the TCP connection is stuck. We can change this in the future, but it will slightly complicate the logic and a ping loop is difficult to test since the pong replies are handled by the websocket library. [1]: ethereum/go-ethereum#27261 [2]: https://github.com/tailscale/tailscale/blob/0f5090c526c2019fae94695b2991cb561e131788/cmd/derper/websocket.go#L13 [3]: coder/websocket#350 (comment) [4]: https://github.com/ethereum/go-ethereum/blob/6d2aeb43d516fbe2cafd9e65df7ccbd885f861d3/rpc/websocket.go#L336-L359 [5]: https://github.com/cometbft/cometbft/blob/c138e785c992b3a76cb25fdf2e0820e29c56c1af/rpc/jsonrpc/server/ws_handler.go#L427-L432 [6]: https://groups.google.com/a/chromium.org/g/net-dev/c/2RAm-ZYAIYY/m/NMFuHBuHAAAJhttps://groups.google.com/a/chromium.org/g/net-dev/c/2RAm-ZYAIYY/m/NMFuHBuHAAAJ [7]: https://github.com/golang/go/blob/f3bf18117b284b63f4350a5aa61773a30d91a6d5/src/net/dial.go#L15-L17
1 parent f5b8630 commit f441387

File tree

4 files changed

+243
-0
lines changed

4 files changed

+243
-0
lines changed

go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ require (
2424
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa
2525
google.golang.org/grpc v1.46.2
2626
google.golang.org/protobuf v1.28.0
27+
nhooyr.io/websocket v1.8.7
2728
)
2829

2930
require gopkg.in/yaml.v2 v2.4.0 // indirect

go.sum

+17
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,11 @@ github.com/getsentry/sentry-go v0.13.0 h1:20dgTiUSfxRB/EhMPtxcL9ZEbM1ZdR+W/7f7NW
164164
github.com/getsentry/sentry-go v0.13.0/go.mod h1:EOsfu5ZdvKPfeHYV6pTVQnsjfp30+XA7//UooKNumH0=
165165
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
166166
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
167+
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
168+
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
167169
github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
170+
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
171+
github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs=
168172
github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98=
169173
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
170174
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
@@ -186,12 +190,16 @@ github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8c
186190
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
187191
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
188192
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
193+
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
189194
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
190195
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
191196
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
192197
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
198+
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
193199
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
200+
github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
194201
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
202+
github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
195203
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
196204
github.com/gogo/googleapis v0.0.0-20180223154316-0cd9801be74a/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
197205
github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4=
@@ -310,6 +318,7 @@ github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCV
310318
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
311319
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
312320
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
321+
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
313322
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
314323
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
315324
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
@@ -335,6 +344,7 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
335344
github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
336345
github.com/klauspost/compress v1.9.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
337346
github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
347+
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
338348
github.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw=
339349
github.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4=
340350
github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
@@ -386,9 +396,11 @@ github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iP
386396
github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU=
387397
github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU=
388398
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
399+
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
389400
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
390401
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
391402
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
403+
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
392404
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
393405
github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ=
394406
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
@@ -517,8 +529,10 @@ github.com/twmb/murmur3 v1.1.6 h1:mqrRot1BRxm+Yct+vavLMou2/iJt0tNVTTC0QoIjaZg=
517529
github.com/twmb/murmur3 v1.1.6/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ=
518530
github.com/tyler-smith/go-bip39 v1.0.1-0.20181017060643-dbb3b84ba2ef h1:wHSqTBrZW24CsNJDfeh9Ex6Pm0Rcpc7qrgKBiL44vF4=
519531
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
532+
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
520533
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
521534
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
535+
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
522536
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
523537
github.com/urfave/cli/v2 v2.23.7 h1:YHDQ46s3VghFHFf1DdF+Sh7H4RqhcM+t0TmZRJx4oJY=
524538
github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4=
@@ -941,6 +955,7 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
941955
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
942956
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
943957
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
958+
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
944959
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
945960
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
946961
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
@@ -956,6 +971,8 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
956971
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
957972
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
958973
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
974+
nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=
975+
nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
959976
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
960977
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
961978
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

jsonrpc/websocket.go

+172
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
package jsonrpc
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"net/http"
8+
"time"
9+
10+
"github.com/NethermindEth/juno/utils"
11+
"nhooyr.io/websocket"
12+
)
13+
14+
type Websocket struct {
15+
rpc *Server
16+
http *http.Server
17+
log utils.SimpleLogger
18+
connParams *WebsocketConnParams
19+
port uint16
20+
}
21+
22+
func NewWebsocket(port uint16, methods []Method, log utils.SimpleLogger) *Websocket {
23+
ws := &Websocket{
24+
rpc: NewServer(),
25+
log: log,
26+
connParams: DefaultWebsocketConnParams(),
27+
port: port,
28+
}
29+
for _, method := range methods {
30+
err := ws.rpc.RegisterMethod(method)
31+
if err != nil {
32+
panic(err)
33+
}
34+
}
35+
return ws
36+
}
37+
38+
// WithConnParams sanity checks and applies the provided params.
39+
func (ws *Websocket) WithConnParams(p *WebsocketConnParams) *Websocket {
40+
if p.PingPeriod < 0 {
41+
return ws
42+
}
43+
ws.connParams = p
44+
return ws
45+
}
46+
47+
// ServeHTTP processes an HTTP request and upgrades it to a websocket connection.
48+
// The connection's entire "lifetime" is spent in this function.
49+
func (ws *Websocket) Handler(ctx context.Context) http.Handler {
50+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
51+
conn, err := websocket.Accept(w, r, nil /* TODO: options */)
52+
if err != nil {
53+
ws.log.Errorw("Failed to upgrade connection", "err", err)
54+
return
55+
}
56+
57+
// TODO include connection information, such as the remote address, in the logs.
58+
59+
wsc := newWebsocketConn(conn, ws.rpc, ws.connParams)
60+
61+
err = wsc.ReadWriteLoop(ctx)
62+
63+
status := websocket.CloseStatus(err)
64+
if status == -1 {
65+
status = websocket.StatusInternalError
66+
}
67+
if err = wsc.conn.Close(status, ""); err != nil {
68+
ws.log.Errorw("Failed to close websocket connection", "err", err)
69+
return
70+
}
71+
})
72+
}
73+
74+
func (ws *Websocket) Run(ctx context.Context) error {
75+
errCh := make(chan error)
76+
77+
srv := &http.Server{
78+
Addr: fmt.Sprintf(":%d", ws.port),
79+
Handler: ws.Handler(ctx),
80+
ReadHeaderTimeout: 1 * time.Second,
81+
}
82+
83+
go func() {
84+
<-ctx.Done()
85+
errCh <- srv.Shutdown(context.Background())
86+
close(errCh)
87+
}()
88+
89+
if err := srv.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
90+
return err
91+
}
92+
93+
return <-errCh
94+
}
95+
96+
type WebsocketConnParams struct {
97+
// How often to send pings to the client.
98+
PingPeriod time.Duration
99+
// Maximum message size allowed.
100+
ReadLimit int64
101+
// Maximum time to write a message.
102+
WriteDuration time.Duration
103+
// Maximum time to read a message.
104+
ReadDuration time.Duration
105+
}
106+
107+
func DefaultWebsocketConnParams() *WebsocketConnParams {
108+
return &WebsocketConnParams{
109+
PingPeriod: (30 * 9 * time.Second) / 10, // 0.9 * 30 seconds
110+
ReadLimit: 32 * 1024 * 1024,
111+
WriteDuration: 5 * time.Second,
112+
ReadDuration: 30 * time.Second,
113+
}
114+
}
115+
116+
type websocketConn struct {
117+
conn *websocket.Conn
118+
rpc *Server
119+
params *WebsocketConnParams
120+
121+
// The ping loop and main loop can both send errors
122+
errChan chan error
123+
}
124+
125+
func newWebsocketConn(conn *websocket.Conn, rpc *Server, params *WebsocketConnParams) *websocketConn {
126+
conn.SetReadLimit(params.ReadLimit)
127+
return &websocketConn{
128+
conn: conn,
129+
rpc: rpc,
130+
params: params,
131+
}
132+
}
133+
134+
func (wsc *websocketConn) ReadWriteLoop(ctx context.Context) error {
135+
for {
136+
// Read next message from the client.
137+
_, r, err := wsc.Read(ctx)
138+
if err != nil {
139+
return err
140+
}
141+
142+
// TODO write responses concurrently. Unlike gorilla/websocket, nhooyr.io/websocket
143+
// permits concurrent writes.
144+
145+
// Decode the message, call the handler, encode the response.
146+
resp, err := wsc.rpc.Handle(r)
147+
if err != nil {
148+
// RPC handling issues should not affect the connection.
149+
// Ignore the request and let the client close the connection.
150+
// TODO: is there a better way to do this?
151+
continue
152+
}
153+
154+
// Send the message to the client.
155+
if err := wsc.Write(ctx, resp); err != nil {
156+
return err
157+
}
158+
}
159+
}
160+
161+
func (wsc *websocketConn) Read(ctx context.Context) (websocket.MessageType, []byte, error) {
162+
readCtx, readCancel := context.WithTimeout(ctx, wsc.params.ReadDuration)
163+
defer readCancel()
164+
return wsc.conn.Read(readCtx)
165+
}
166+
167+
func (wsc *websocketConn) Write(ctx context.Context, msg []byte) error {
168+
writeCtx, writeCancel := context.WithTimeout(ctx, wsc.params.WriteDuration)
169+
defer writeCancel()
170+
// Use MessageText since JSON is a text format.
171+
return wsc.conn.Write(writeCtx, websocket.MessageText, msg)
172+
}

jsonrpc/websocket_test.go

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package jsonrpc_test
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net/http"
7+
"testing"
8+
9+
"github.com/NethermindEth/juno/jsonrpc"
10+
"github.com/NethermindEth/juno/utils"
11+
"github.com/stretchr/testify/assert"
12+
"github.com/stretchr/testify/require"
13+
"nhooyr.io/websocket"
14+
)
15+
16+
// The caller is responsible for closing the connection.
17+
func testConnection(t *testing.T, port uint16) *websocket.Conn {
18+
methods := []jsonrpc.Method{{
19+
Name: "test_echo",
20+
Params: []jsonrpc.Parameter{{Name: "msg"}},
21+
Handler: func(msg string) (string, *jsonrpc.Error) {
22+
return msg, nil
23+
},
24+
}}
25+
ctx := context.Background()
26+
ws := jsonrpc.NewWebsocket(port, methods, utils.NewNopZapLogger())
27+
go func() {
28+
t.Helper()
29+
require.NoError(t, ws.Run(context.Background()))
30+
}()
31+
32+
url := fmt.Sprintf("ws://localhost:%d", port)
33+
conn, resp, err := websocket.Dial(ctx, url, nil)
34+
require.NoError(t, err)
35+
require.Equal(t, http.StatusSwitchingProtocols, resp.StatusCode)
36+
37+
return conn
38+
}
39+
40+
func TestHandler(t *testing.T) {
41+
conn := testConnection(t, 8457)
42+
43+
msg := `{"jsonrpc" : "2.0", "method" : "test_echo", "params" : [ "abc123" ], "id" : 1}`
44+
err := conn.Write(context.Background(), websocket.MessageText, []byte(msg))
45+
require.NoError(t, err)
46+
47+
want := `{"jsonrpc":"2.0","result":"abc123","id":1}`
48+
_, got, err := conn.Read(context.Background())
49+
require.NoError(t, err)
50+
assert.Equal(t, want, string(got))
51+
52+
require.NoError(t, conn.Close(websocket.StatusNormalClosure, ""))
53+
}

0 commit comments

Comments
 (0)