From f0eebfe60c61b9e1c5c9e15a0b5b97a4353c0575 Mon Sep 17 00:00:00 2001 From: Keegan Carruthers-Smith Date: Fri, 31 Jul 2026 11:30:02 +0200 Subject: [PATCH 1/5] chore/ci: stop requiring unauthenticated SCIP uploads Sourcegraph no longer accepts the workflow's GitHub token as upload authentication, so every push reports a failure after indexing succeeds. Keeping this upload would require a separately managed Sourcegraph access token; this repository does not otherwise need that credential, and comparable OSS repositories such as zoekt do not publish SCIP indexes from CI. Remove the obsolete workflow rather than leaving a permanently failing check or masking upload errors. Co-authored-by: Amp Amp-Thread-ID: https://ampcode.com/threads/T-019fb780-ec22-75e7-91a7-34c027582456 --- .github/workflows/scip.yml | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 .github/workflows/scip.yml diff --git a/.github/workflows/scip.yml b/.github/workflows/scip.yml deleted file mode 100644 index 78d5d1b..0000000 --- a/.github/workflows/scip.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: SCIP -'on': - - push -permissions: - contents: read -jobs: - scip-go: - runs-on: ubuntu-latest - container: sourcegraph/scip-go - steps: - - uses: actions/checkout@v7 - - name: Get src-cli - run: curl -L https://sourcegraph.com/.api/src-cli/src_linux_amd64 -o /usr/local/bin/src; - chmod +x /usr/local/bin/src - - name: Set directory to safe for git - run: git config --global --add safe.directory $GITHUB_WORKSPACE - - name: Generate SCIP data - run: scip-go - - name: Upload SCIP data - run: src code-intel upload -github-token=${{ secrets.GITHUB_TOKEN }} From fb2b860c84d6ba28813b6aa96ec9dc50391eb8e1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Aug 2026 10:07:08 +0200 Subject: [PATCH 2/5] Bump actions/setup-go from 6 to 7 in the github-actions group (#101) --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 863d7fb..8fc1d7c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: steps: - uses: actions/checkout@v7 - name: Set up Go - uses: actions/setup-go@v6 + uses: actions/setup-go@v7 with: go-version: ${{ matrix.go }} id: go From bf8fe04036eb93a53c06d7682fc4316da3e5cd24 Mon Sep 17 00:00:00 2001 From: Keegan Carruthers-Smith Date: Thu, 10 Sep 2026 09:43:30 +0200 Subject: [PATCH 3/5] docs/conn: clarify ordering guarantees for bidirectional calls (#105) Users can otherwise mistake transport ordering for application-level synchronization and attempt blocking calls from the synchronous read loop. --- async.go | 3 ++- conn.go | 4 +++- jsonrpc2.go | 10 ++++++---- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/async.go b/async.go index bc8a370..6cbdadd 100644 --- a/async.go +++ b/async.go @@ -3,7 +3,8 @@ package jsonrpc2 import "context" // AsyncHandler wraps a Handler such that each request is handled in its own -// goroutine. It is a convenience wrapper. +// goroutine. Handlers may therefore start or finish in a different order than +// requests are received. It is a convenience wrapper. func AsyncHandler(h Handler) Handler { return asyncHandler{h} } diff --git a/conn.go b/conn.go index a9b14ac..1e29546 100644 --- a/conn.go +++ b/conn.go @@ -156,7 +156,9 @@ func (c *Conn) Notify(ctx context.Context, method string, params interface{}, op return err } -// Reply sends a successful response with a result. +// Reply sends a successful response with a result. It returns after writing +// the response to the ObjectStream; it does not wait for the peer to receive or +// process the response. func (c *Conn) Reply(ctx context.Context, id ID, result interface{}) error { resp := &Response{ID: id} if err := resp.SetResult(result); err != nil { diff --git a/jsonrpc2.go b/jsonrpc2.go index 7d3e132..8c145f2 100644 --- a/jsonrpc2.go +++ b/jsonrpc2.go @@ -59,10 +59,12 @@ const ( // Handler handles JSON-RPC requests and notifications. type Handler interface { - // Handle is called to handle a request. No other requests are handled until - // it returns. If you do not require strict ordering behavior of received - // RPCs, it is suggested to wrap your handler in AsyncHandler. The context - // is automatically canceled when the connection closes. + // Handle is called to handle a request. While it is running, the connection + // does not process any other requests or responses. A handler that waits for + // a call's response must therefore run asynchronously. If you do not require + // strict ordering behavior of received RPCs, it is suggested to wrap your + // handler in AsyncHandler. The context is automatically canceled when the + // connection closes. Handle(context.Context, *Conn, *Request) } From ae9a040819a1b608ceedeae080065f1c4be796b4 Mon Sep 17 00:00:00 2001 From: Keegan Carruthers-Smith Date: Thu, 10 Sep 2026 09:44:07 +0200 Subject: [PATCH 4/5] fix/conn: prevent close-response race panic (#104) Closing pending calls must also remove them from response routing; otherwise a response already read from the stream can later send into a closed waiter channel. Keep registration and shutdown mutually exclusive so the pending map retains that invariant. --- conn.go | 7 ++++- conn_test.go | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/conn.go b/conn.go index 1e29546..ef83a31 100644 --- a/conn.go +++ b/conn.go @@ -188,8 +188,9 @@ func (c *Conn) close(cause error) error { } c.closed = true - for _, call := range c.pending { + for id, call := range c.pending { close(call.done) + delete(c.pending, id) } close(c.disconnect) @@ -320,6 +321,10 @@ func (c *Conn) send(_ context.Context, m *anyMessage, wait bool) (cc *call, err // responses. if m.request != nil && wait { c.mu.Lock() + if c.closed { + c.mu.Unlock() + return nil, ErrClosed + } id = m.request.ID c.pending[id] = cc c.mu.Unlock() diff --git a/conn_test.go b/conn_test.go index d1701da..b4d5c7f 100644 --- a/conn_test.go +++ b/conn_test.go @@ -275,6 +275,78 @@ func TestConn_Close(t *testing.T) { } } +func TestConn_CloseAfterResponseRead(t *testing.T) { + client, server := net.Pipe() + responseRead := make(chan struct{}) + processResponse := make(chan struct{}) + responseIgnored := make(chan struct{}) + var resumeResponse sync.Once + resume := func() { resumeResponse.Do(func() { close(processResponse) }) } + defer resume() + + clientStream := &pauseAfterReadStream{ + ObjectStream: jsonrpc2.NewPlainObjectStream(client), + read: responseRead, + resume: processResponse, + } + clientConn := jsonrpc2.NewConn( + context.Background(), + clientStream, + noopHandler{}, + jsonrpc2.SetLogger(signalLogger{called: responseIgnored}), + ) + serverConn := jsonrpc2.NewConn( + context.Background(), + jsonrpc2.NewPlainObjectStream(server), + handlerFunc(func(ctx context.Context, conn *jsonrpc2.Conn, req *jsonrpc2.Request) { + if err := conn.Reply(ctx, req.ID, nil); err != nil { + t.Error(err) + } + }), + ) + defer serverConn.Close() + + call, err := clientConn.DispatchCall(context.Background(), "m", nil) + if err != nil { + t.Fatal(err) + } + <-responseRead + + if err := clientConn.Close(); err != nil { + t.Fatal(err) + } + resume() + <-responseIgnored + + if err := call.Wait(context.Background(), nil); err != jsonrpc2.ErrClosed { + t.Fatalf("got error %v, want %v", err, jsonrpc2.ErrClosed) + } +} + +type pauseAfterReadStream struct { + jsonrpc2.ObjectStream + read chan<- struct{} + resume <-chan struct{} + once sync.Once +} + +func (s *pauseAfterReadStream) ReadObject(v interface{}) error { + err := s.ObjectStream.ReadObject(v) + s.once.Do(func() { + close(s.read) + <-s.resume + }) + return err +} + +type signalLogger struct { + called chan<- struct{} +} + +func (l signalLogger) Printf(string, ...interface{}) { + close(l.called) +} + func testParams(t *testing.T, want *json.RawMessage, fn func(c *jsonrpc2.Conn) error) { wg := &sync.WaitGroup{} handler := handlerFunc(func(ctx context.Context, conn *jsonrpc2.Conn, r *jsonrpc2.Request) { From f50bb34c705b200ce20e9a81412e1b81167c18ae Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 09:45:16 +0200 Subject: [PATCH 5/5] Bump github.com/gorilla/websocket from 1.4.1 to 1.5.3 (#102) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 95cca53..f7cd1b0 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,4 @@ module github.com/sourcegraph/jsonrpc2 go 1.12 -require github.com/gorilla/websocket v1.4.1 +require github.com/gorilla/websocket v1.5.3 diff --git a/go.sum b/go.sum index 1f9b923..25a9fc4 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,2 @@ -github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= -github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=