1
0
Fork 0
mirror of https://github.com/sourcegraph/jsonrpc2.git synced 2026-06-16 20:20:03 +02:00

Compare commits

..

17 commits

Author SHA1 Message Date
dependabot[bot]
4756698b1e
Bump actions/checkout from 5 to 6 in the github-actions group (#93) 2025-12-02 09:01:38 +02:00
dependabot[bot]
ef3ea8b2ea
Bump actions/setup-go from 5 to 6 in the github-actions group (#92)
Bumps the github-actions group with 1 update: [actions/setup-go](https://github.com/actions/setup-go).


Updates `actions/setup-go` from 5 to 6
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-02 20:55:33 +02:00
dependabot[bot]
3c4c92ad61
Bump actions/checkout from 4 to 5 in the github-actions group (#91)
Bumps the github-actions group with 1 update: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 4 to 5
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-02 13:36:38 +02:00
Sam Herrmann
ddb146fd0d
Cancel Handler context when connection closes (#90) 2025-08-19 16:19:52 +02:00
Kevin Gillette
2cc94179e1
transparently simplify control flow (#83) 2025-02-17 16:55:54 +02:00
Noah S-C
534fd43609
Merge pull request #80 from sourcegraph/nsc/lsifgo-to-scipgo
chore: use scip-go instead of lsif-go for precise indexing in CI
2024-02-23 16:31:37 +00:00
Noah S-C
4963d1c241 chore: use scip-go instead of lsif-go for precise indexing in CI 2024-02-23 15:34:49 +00:00
dependabot[bot]
dd69e185fa
Bump the github-actions group with 2 updates (#79)
Updates `actions/checkout` from 1 to 4

Updates `actions/setup-go` from 2 to 5

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-10 12:16:40 +02:00
Joyce
bf47ec21a6
ci: Create dependabot.yml (#78)
Signed-off-by: Joyce <joycebrum@google.com>
2024-01-10 12:06:38 +02:00
Will Dollman
cd64a673da
Merge pull request #75 from diogoteles08/patch-1
Docs: Create Security Policy
2023-09-25 12:04:20 +01:00
Will Dollman
943e53c8e9
Update SECURITY.md
Co-authored-by: Vincent <evict@users.noreply.github.com>
2023-09-25 11:53:41 +01:00
Will Dollman
e4e2e6324c Update security policy to use email for reporting 2023-09-25 11:50:00 +01:00
Diogo Teles Sant'Anna
510183e882
Create Security Policy 2023-09-11 14:51:22 -03:00
Diogo Teles Sant'Anna
8a0bf06edf
ci: set minimal permissions to GitHub workflows (#73)
Signed-off-by: Diogo Teles Sant'Anna <diogoteles@google.com>
2023-08-30 09:07:57 +02:00
Fazlul Shahriar
b9c1fbdb96
Fix logging of received response messages (#71)
As documented: OnRecv causes all requests received on conn to invoke
f(req, nil) and all responses to invoke f(req, resp).

Since OnRecv is called with both *Request and *Response being non-nil
when we're handling a response, we need to check that *Response is
non-nil before we check *Request is non-nil. This change just swaps the
two cases in the switch statement to fix the issue. For consistency,
I've swapped the cases for OnSend also, even when it's not needed.
2023-07-14 13:00:57 +02:00
Keegan Carruthers-Smith
5d80b29f44
conn: do not lock sending when closing (#70)
The sending mutex may be blocked due to the underlying conn blocking. If
that happens then we can't call close since close also attempts to hold
the sending mutex. Sending mutex is only used for serializing sends and
doesn't protect the fields close reads/writes. I believe we introduced
locking it so we would return ErrClosed. This commit instead introduces
a check in send which rechecks if we have since closed while attempting
to send.

Test Plan: expanded the test coverage
2023-06-07 08:40:20 +02:00
Sam Herrmann
040dc22f8a
Add package example test (#68) 2023-03-01 06:46:15 +02:00
15 changed files with 500 additions and 237 deletions

10
.github/dependabot.yml vendored Normal file
View file

@ -0,0 +1,10 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"
groups:
github-actions:
patterns:
- "*"

View file

@ -4,6 +4,10 @@ on:
push: push:
branches: branches:
- master - master
permissions:
contents: read
jobs: jobs:
test: test:
strategy: strategy:
@ -14,9 +18,9 @@ jobs:
name: Go ${{ matrix.go }} name: Go ${{ matrix.go }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v6
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v2 uses: actions/setup-go@v6
with: with:
go-version: ${{ matrix.go }} go-version: ${{ matrix.go }}
id: go id: go

View file

@ -1,13 +0,0 @@
name: LSIF
on:
- push
jobs:
lsif-go:
runs-on: ubuntu-latest
container: sourcegraph/lsif-go
steps:
- uses: actions/checkout@v1
- name: Generate LSIF data
run: lsif-go
- name: Upload LSIF data
run: src lsif upload -github-token=${{ secrets.GITHUB_TOKEN }}

20
.github/workflows/scip.yml vendored Normal file
View file

@ -0,0 +1,20 @@
name: SCIP
'on':
- push
permissions:
contents: read
jobs:
scip-go:
runs-on: ubuntu-latest
container: sourcegraph/scip-go
steps:
- uses: actions/checkout@v6
- 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 }}

View file

@ -3,9 +3,8 @@
Package jsonrpc2 provides a [Go](https://golang.org) implementation of [JSON-RPC 2.0](http://www.jsonrpc.org/specification). Package jsonrpc2 provides a [Go](https://golang.org) implementation of [JSON-RPC 2.0](http://www.jsonrpc.org/specification).
This package is **experimental** until further notice. * [Documentation](https://pkg.go.dev/github.com/sourcegraph/jsonrpc2)
* [Open the code in Sourcegraph](https://sourcegraph.com/github.com/sourcegraph/jsonrpc2)
[**Open the code in Sourcegraph**](https://sourcegraph.com/github.com/sourcegraph/jsonrpc2)
## Known issues ## Known issues

12
SECURITY.md Normal file
View file

@ -0,0 +1,12 @@
# Security Policy
## Supported Versions
Security updates are applied only to the latest release.
## Reporting a Vulnerability
If you have discovered a security vulnerability in this project, please report it privately. **Do not disclose it as a public issue.** This gives us time to work with you to evaluate and fix the issue before public exposure, reducing the chance that the exploit will be used before a patch is released.
Please disclose it privately via email to security@sourcegraph.com. We will work with you to understand and resolve the issue promptly.

136
conn.go
View file

@ -27,6 +27,7 @@ type Conn struct {
sending sync.Mutex sending sync.Mutex
cancelCtx context.CancelFunc
disconnect chan struct{} disconnect chan struct{}
logger Logger logger Logger
@ -43,13 +44,19 @@ var _ JSONRPC2 = (*Conn)(nil)
// JSON-RPC protocol is symmetric, so a Conn runs on both ends of a // JSON-RPC protocol is symmetric, so a Conn runs on both ends of a
// client-server connection. // client-server connection.
// //
// NewClient consumes conn, so you should call Close on the returned // NewConn consumes stream, so you should call Close on the returned
// client not on the given conn. // Conn not on the given stream or its underlying connection.
//
// Conn is closed when the given context's Done channel is closed.
func NewConn(ctx context.Context, stream ObjectStream, h Handler, opts ...ConnOpt) *Conn { func NewConn(ctx context.Context, stream ObjectStream, h Handler, opts ...ConnOpt) *Conn {
ctx, cancel := context.WithCancel(ctx)
c := &Conn{ c := &Conn{
stream: stream, stream: stream,
h: h, h: h,
pending: map[ID]*call{}, pending: map[ID]*call{},
cancelCtx: cancel,
disconnect: make(chan struct{}), disconnect: make(chan struct{}),
logger: log.New(os.Stderr, "", log.LstdFlags), logger: log.New(os.Stderr, "", log.LstdFlags),
} }
@ -60,6 +67,12 @@ func NewConn(ctx context.Context, stream ObjectStream, h Handler, opts ...ConnOp
opt(c) opt(c)
} }
go c.readMessages(ctx) go c.readMessages(ctx)
go func() {
<-ctx.Done()
c.close(nil)
}()
return c return c
} }
@ -166,9 +179,7 @@ func (c *Conn) SendResponse(ctx context.Context, resp *Response) error {
} }
func (c *Conn) close(cause error) error { func (c *Conn) close(cause error) error {
c.sending.Lock()
c.mu.Lock() c.mu.Lock()
defer c.sending.Unlock()
defer c.mu.Unlock() defer c.mu.Unlock()
if c.closed { if c.closed {
@ -184,20 +195,23 @@ func (c *Conn) close(cause error) error {
} }
close(c.disconnect) close(c.disconnect)
c.cancelCtx()
c.closed = true c.closed = true
return c.stream.Close() return c.stream.Close()
} }
func (c *Conn) readMessages(ctx context.Context) { func (c *Conn) readMessages(ctx context.Context) {
var err error for {
for err == nil {
var m anyMessage var m anyMessage
err = c.stream.ReadObject(&m) err := c.stream.ReadObject(&m)
if err != nil { if err != nil {
break c.close(err)
return
} }
switch { switch {
// TODO: handle the case where both request and response are nil.
case m.request != nil: case m.request != nil:
for _, onRecv := range c.onRecv { for _, onRecv := range c.onRecv {
onRecv(m.request, nil) onRecv(m.request, nil)
@ -206,49 +220,53 @@ func (c *Conn) readMessages(ctx context.Context) {
case m.response != nil: case m.response != nil:
resp := m.response resp := m.response
if resp != nil { id := resp.ID
id := resp.ID c.mu.Lock()
c.mu.Lock() call := c.pending[id]
call := c.pending[id] delete(c.pending, id)
delete(c.pending, id) c.mu.Unlock()
c.mu.Unlock()
if call != nil { var req *Request
call.response = resp if call != nil {
} call.response = resp
req = call.request
if len(c.onRecv) > 0 {
var req *Request
if call != nil {
req = call.request
}
for _, onRecv := range c.onRecv {
onRecv(req, resp)
}
}
switch {
case call == nil:
c.logger.Printf("jsonrpc2: ignoring response #%s with no corresponding request\n", id)
case resp.Error != nil:
call.done <- resp.Error
close(call.done)
default:
call.done <- nil
close(call.done)
}
} }
for _, onRecv := range c.onRecv {
onRecv(req, resp)
}
if call == nil {
c.logger.Printf("jsonrpc2: ignoring response #%s with no corresponding request\n", id)
continue
}
var err error
if resp.Error != nil {
err = resp.Error
}
call.done <- err
close(call.done)
} }
} }
c.close(err)
} }
func (c *Conn) send(_ context.Context, m *anyMessage, wait bool) (cc *call, err error) { func (c *Conn) send(_ context.Context, m *anyMessage, wait bool) (cc *call, err error) {
c.sending.Lock() c.sending.Lock()
defer c.sending.Unlock() defer c.sending.Unlock()
// double check the error isn't due to being closed while sending.
defer func() {
if err != nil {
c.mu.Lock()
if c.closed {
err = ErrClosed
}
c.mu.Unlock()
}
}()
// m.request.ID could be changed, so we store a copy to correctly // m.request.ID could be changed, so we store a copy to correctly
// clean up pending // clean up pending
var id ID var id ID
@ -330,25 +348,20 @@ type Waiter struct {
// error is returned. // error is returned.
func (w Waiter) Wait(ctx context.Context, result interface{}) error { func (w Waiter) Wait(ctx context.Context, result interface{}) error {
select { select {
case err, ok := <-w.call.done:
if !ok {
err = ErrClosed
}
if err != nil {
return err
}
if result != nil {
if w.call.response.Result == nil {
w.call.response.Result = &jsonNull
}
if err := json.Unmarshal(*w.call.response.Result, result); err != nil {
return err
}
}
return nil
case <-ctx.Done(): case <-ctx.Done():
return ctx.Err() return ctx.Err()
case err, ok := <-w.call.done:
if !ok {
return ErrClosed
}
if err != nil || result == nil {
return err
}
if w.call.response.Result == nil {
w.call.response.Result = &jsonNull
}
return json.Unmarshal(*w.call.response.Result, result)
} }
} }
@ -414,12 +427,7 @@ func (m *anyMessage) UnmarshalJSON(data []byte) error {
return errors.New("jsonrpc2: invalid empty batch") return errors.New("jsonrpc2: invalid empty batch")
} }
for i := range msgs { for i := range msgs {
if err := checkType(&msg{ if err := checkType(&msgs[i]); err != nil {
ID: msgs[i].ID,
Method: msgs[i].Method,
Result: msgs[i].Result,
Error: msgs[i].Error,
}); err != nil {
return err return err
} }
} }

View file

@ -43,6 +43,20 @@ func LogMessages(logger Logger) ConnOpt {
OnRecv(func(req *Request, resp *Response) { OnRecv(func(req *Request, resp *Response) {
switch { switch {
case resp != nil:
method := "(no matching request)"
if req != nil {
method = req.Method
}
switch {
case resp.Result != nil:
result, _ := json.Marshal(resp.Result)
logger.Printf("jsonrpc2: --> result #%s: %s: %s\n", resp.ID, method, result)
case resp.Error != nil:
err, _ := json.Marshal(resp.Error)
logger.Printf("jsonrpc2: --> error #%s: %s: %s\n", resp.ID, method, err)
}
case req != nil: case req != nil:
mu.Lock() mu.Lock()
reqMethods[req.ID] = req.Method reqMethods[req.ID] = req.Method
@ -54,34 +68,10 @@ func LogMessages(logger Logger) ConnOpt {
} else { } else {
logger.Printf("jsonrpc2: --> request #%s: %s: %s\n", req.ID, req.Method, params) logger.Printf("jsonrpc2: --> request #%s: %s: %s\n", req.ID, req.Method, params)
} }
case resp != nil:
var method string
if req != nil {
method = req.Method
} else {
method = "(no matching request)"
}
switch {
case resp.Result != nil:
result, _ := json.Marshal(resp.Result)
logger.Printf("jsonrpc2: --> result #%s: %s: %s\n", resp.ID, method, result)
case resp.Error != nil:
err, _ := json.Marshal(resp.Error)
logger.Printf("jsonrpc2: --> error #%s: %s: %s\n", resp.ID, method, err)
}
} }
})(c) })(c)
OnSend(func(req *Request, resp *Response) { OnSend(func(req *Request, resp *Response) {
switch { switch {
case req != nil:
params, _ := json.Marshal(req.Params)
if req.Notif {
logger.Printf("jsonrpc2: <-- notif: %s: %s\n", req.Method, params)
} else {
logger.Printf("jsonrpc2: <-- request #%s: %s: %s\n", req.ID, req.Method, params)
}
case resp != nil: case resp != nil:
mu.Lock() mu.Lock()
method := reqMethods[resp.ID] method := reqMethods[resp.ID]
@ -98,6 +88,14 @@ func LogMessages(logger Logger) ConnOpt {
err, _ := json.Marshal(resp.Error) err, _ := json.Marshal(resp.Error)
logger.Printf("jsonrpc2: <-- error #%s: %s: %s\n", resp.ID, method, err) logger.Printf("jsonrpc2: <-- error #%s: %s: %s\n", resp.ID, method, err)
} }
case req != nil:
params, _ := json.Marshal(req.Params)
if req.Notif {
logger.Printf("jsonrpc2: <-- notif: %s: %s\n", req.Method, params)
} else {
logger.Printf("jsonrpc2: <-- request #%s: %s: %s\n", req.ID, req.Method, params)
}
} }
})(c) })(c)
} }

View file

@ -51,3 +51,80 @@ func TestSetLogger(t *testing.T) {
t.Fatalf("got %q, want %q", got, want) t.Fatalf("got %q, want %q", got, want)
} }
} }
type dummyHandler struct {
t *testing.T
}
func (h *dummyHandler) Handle(ctx context.Context, conn *jsonrpc2.Conn, req *jsonrpc2.Request) {
if !req.Notif {
err := conn.Reply(ctx, req.ID, nil)
if err != nil {
h.t.Error(err)
return
}
}
}
func TestLogMessages(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
rd, wr := io.Pipe()
defer rd.Close()
defer wr.Close()
buf := bufio.NewReader(rd)
logger := log.New(wr, "", log.Lmsgprefix)
a, b := net.Pipe()
connA := jsonrpc2.NewConn(
ctx,
jsonrpc2.NewBufferedStream(a, jsonrpc2.VSCodeObjectCodec{}),
&dummyHandler{t},
jsonrpc2.LogMessages(logger),
)
connB := jsonrpc2.NewConn(
ctx,
jsonrpc2.NewBufferedStream(b, jsonrpc2.VSCodeObjectCodec{}),
&dummyHandler{t},
)
defer connA.Close()
defer connB.Close()
go func() {
if err := connA.Call(ctx, "method1", nil, nil); err != nil {
t.Error(err)
return
}
if err := connB.Call(ctx, "method2", nil, nil); err != nil {
t.Error(err)
return
}
if err := connA.Notify(ctx, "notification1", nil); err != nil {
t.Error(err)
return
}
if err := connB.Notify(ctx, "notification2", nil); err != nil {
t.Error(err)
return
}
}()
for i, want := range []string{
"jsonrpc2: <-- request #0: method1: null\n",
"jsonrpc2: --> result #0: method1: null\n",
"jsonrpc2: --> request #0: method2: null\n",
"jsonrpc2: <-- result #0: method2: null\n",
"jsonrpc2: <-- notif: notification1: null\n",
"jsonrpc2: --> notif: notification2: null\n",
} {
got, err := buf.ReadString('\n')
if err != nil {
t.Fatal(err)
}
if got != want {
t.Errorf("message %v: got %q, want %q", i, got, want)
}
}
}

View file

@ -14,6 +14,58 @@ import (
"github.com/sourcegraph/jsonrpc2" "github.com/sourcegraph/jsonrpc2"
) )
func TestConn(t *testing.T) {
t.Run("closes when context is done", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
connA, connB := Pipe(ctx, noopHandler{}, noopHandler{})
defer connA.Close()
defer connB.Close()
cancel()
<-connA.DisconnectNotify()
got := connA.Close()
want := jsonrpc2.ErrClosed
if got != want {
t.Fatalf("got %v, want %v", got, want)
}
})
t.Run("cancels context when closed", func(t *testing.T) {
ctxCanceled := make(chan struct{})
handler := handlerFunc(func(ctx context.Context, c *jsonrpc2.Conn, r *jsonrpc2.Request) {
// Block until the context is canceled.
<-ctx.Done()
close(ctxCanceled)
})
connA, connB := Pipe(context.Background(), noopHandler{}, jsonrpc2.AsyncHandler(handler))
defer connA.Close()
defer connB.Close()
// Send a notification from connA to connB to trigger connB's handler
// function.
if err := connA.Notify(context.Background(), "foo", nil, nil); err != nil {
t.Fatal(err)
}
// Disconnect connA from connB.
if err := connA.Close(); err != nil {
t.Fatal(err)
}
select {
case <-ctxCanceled:
// Test passed, the handler's context was canceled.
case <-time.After(time.Second):
t.Fatal("context not canceled")
}
})
}
var paramsTests = []struct { var paramsTests = []struct {
sendParams interface{} sendParams interface{}
wantParams *json.RawMessage wantParams *json.RawMessage
@ -118,38 +170,77 @@ func TestConn_DisconnectNotify(t *testing.T) {
} }
func TestConn_Close(t *testing.T) { func TestConn_Close(t *testing.T) {
t.Run("waiting for response", func(t *testing.T) { cases := []struct {
connA, connB := net.Pipe() name string
nodeA := jsonrpc2.NewConn( run func(*testing.T, context.Context, *jsonrpc2.Conn)
context.Background(), }{{
jsonrpc2.NewPlainObjectStream(connA), noopHandler{}, name: "during Call",
) run: func(t *testing.T, ctx context.Context, conn *jsonrpc2.Conn) {
defer nodeA.Close() ready := make(chan struct{})
nodeB := jsonrpc2.NewConn( done := make(chan struct{})
context.Background(), go func() {
jsonrpc2.NewPlainObjectStream(connB), close(ready)
noopHandler{}, err := conn.Call(ctx, "m", nil, nil)
) if err != jsonrpc2.ErrClosed {
defer nodeB.Close() t.Errorf("got error %v, want %v", err, jsonrpc2.ErrClosed)
}
ready := make(chan struct{}) close(done)
done := make(chan struct{}) }()
go func() { // Wait for the request to be sent before we close the connection.
close(ready) <-ready
err := nodeB.Call(context.Background(), "m", nil, nil) if err := conn.Close(); err != nil && err != jsonrpc2.ErrClosed {
if err != jsonrpc2.ErrClosed { t.Error(err)
t.Errorf("got error %v, want %v", err, jsonrpc2.ErrClosed)
} }
close(done) <-done
}() },
// Wait for the request to be sent before we close the connection. }, {
<-ready name: "during Wait",
if err := nodeB.Close(); err != nil && err != jsonrpc2.ErrClosed { run: func(t *testing.T, ctx context.Context, conn *jsonrpc2.Conn) {
t.Error(err) call, err := conn.DispatchCall(ctx, "m", nil, nil)
} if err != nil {
assertDisconnect(t, nodeB, connB) t.Fatal(err)
<-done }
}) if err := conn.Close(); err != nil {
t.Fatal(err)
}
if err := call.Wait(ctx, nil); err != jsonrpc2.ErrClosed {
t.Fatal(err)
}
},
}, {
name: "during Dispatch",
run: func(t *testing.T, ctx context.Context, conn *jsonrpc2.Conn) {
if err := conn.Close(); err != nil {
t.Fatal(err)
}
if _, err := conn.DispatchCall(ctx, "m", nil, nil); err != jsonrpc2.ErrClosed {
t.Fatal(err)
}
},
}}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
connA, connB := net.Pipe()
nodeA := jsonrpc2.NewConn(
ctx,
jsonrpc2.NewPlainObjectStream(connA), noopHandler{},
)
defer nodeA.Close()
nodeB := jsonrpc2.NewConn(
ctx,
jsonrpc2.NewPlainObjectStream(connB),
noopHandler{},
)
defer nodeB.Close()
tc.run(t, ctx, nodeB)
assertDisconnect(t, nodeB, connB)
})
}
} }
func testParams(t *testing.T, want *json.RawMessage, fn func(c *jsonrpc2.Conn) error) { func testParams(t *testing.T, want *json.RawMessage, fn func(c *jsonrpc2.Conn) error) {
@ -159,12 +250,12 @@ func testParams(t *testing.T, want *json.RawMessage, fn func(c *jsonrpc2.Conn) e
wg.Done() wg.Done()
}) })
client, server := newClientServer(handler) connA, connB := Pipe(context.Background(), noopHandler{}, handler)
defer client.Close() defer connA.Close()
defer server.Close() defer connB.Close()
wg.Add(1) wg.Add(1)
if err := fn(client); err != nil { if err := fn(connA); err != nil {
t.Error(err) t.Error(err)
} }
wg.Wait() wg.Wait()
@ -203,18 +294,11 @@ func assertRawJSONMessage(t *testing.T, got *json.RawMessage, want *json.RawMess
} }
} }
func newClientServer(handler jsonrpc2.Handler) (client *jsonrpc2.Conn, server *jsonrpc2.Conn) { // Pipe returns two jsonrpc2.Conn, connected via a synchronous, in-memory, full
ctx := context.Background() // duplex network connection.
connA, connB := net.Pipe() func Pipe(ctx context.Context, handlerA, handlerB jsonrpc2.Handler) (connA *jsonrpc2.Conn, connB *jsonrpc2.Conn) {
client = jsonrpc2.NewConn( a, b := net.Pipe()
ctx, connA = jsonrpc2.NewConn(ctx, jsonrpc2.NewPlainObjectStream(a), handlerA)
jsonrpc2.NewPlainObjectStream(connA), connB = jsonrpc2.NewConn(ctx, jsonrpc2.NewPlainObjectStream(b), handlerB)
noopHandler{}, return connA, connB
)
server = jsonrpc2.NewConn(
ctx,
jsonrpc2.NewPlainObjectStream(connB),
handler,
)
return client, server
} }

78
example_params_test.go Normal file
View file

@ -0,0 +1,78 @@
package jsonrpc2_test
import (
"context"
"encoding/json"
"fmt"
"net"
"os"
"github.com/sourcegraph/jsonrpc2"
)
// Send a JSON-RPC notification with its params member omitted.
func ExampleConn_Notify_paramsOmitted() {
ctx := context.Background()
connA, connB := net.Pipe()
defer connA.Close()
defer connB.Close()
rpcConn := jsonrpc2.NewConn(ctx, jsonrpc2.NewPlainObjectStream(connA), nil)
// Send the JSON-RPC notification.
go func() {
// Set params to nil.
if err := rpcConn.Notify(ctx, "foo", nil); err != nil {
fmt.Fprintln(os.Stderr, "notify:", err)
}
}()
// Read the raw JSON-RPC notification on connB.
//
// Reading the raw JSON-RPC request is for the purpose of this example only.
// Use a jsonrpc2.Handler to read parsed requests.
buf := make([]byte, 64)
n, err := connB.Read(buf)
if err != nil {
fmt.Fprintln(os.Stderr, "read:", err)
}
fmt.Printf("%s\n", buf[:n])
// Output: {"jsonrpc":"2.0","method":"foo"}
}
// Send a JSON-RPC notification with its params member set to null.
func ExampleConn_Notify_nullParams() {
ctx := context.Background()
connA, connB := net.Pipe()
defer connA.Close()
defer connB.Close()
rpcConn := jsonrpc2.NewConn(ctx, jsonrpc2.NewPlainObjectStream(connA), nil)
// Send the JSON-RPC notification.
go func() {
// Set params to the JSON null value.
params := json.RawMessage("null")
if err := rpcConn.Notify(ctx, "foo", params); err != nil {
fmt.Fprintln(os.Stderr, "notify:", err)
}
}()
// Read the raw JSON-RPC notification on connB.
//
// Reading the raw JSON-RPC request is for the purpose of this example only.
// Use a jsonrpc2.Handler to read parsed requests.
buf := make([]byte, 64)
n, err := connB.Read(buf)
if err != nil {
fmt.Fprintln(os.Stderr, "read:", err)
}
fmt.Printf("%s\n", buf[:n])
// Output: {"jsonrpc":"2.0","method":"foo","params":null}
}

View file

@ -2,77 +2,63 @@ package jsonrpc2_test
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"log"
"net" "net"
"os" "os"
"github.com/sourcegraph/jsonrpc2" "github.com/sourcegraph/jsonrpc2"
) )
// Send a JSON-RPC notification with its params member omitted. func Example() {
func ExampleConn_Notify_paramsOmitted() {
ctx := context.Background() ctx := context.Background()
// Create an in-memory network connection. This connection is used below to
// transport the JSON-RPC messages. However, any io.ReadWriteCloser may be
// used to send/receive JSON-RPC messages.
connA, connB := net.Pipe() connA, connB := net.Pipe()
defer connA.Close()
defer connB.Close()
rpcConn := jsonrpc2.NewConn(ctx, jsonrpc2.NewPlainObjectStream(connA), nil) // The following JSON-RPC connection is both a client and a server. It can
// send requests as well as receive requests. The incoming requests are
// handled by myHandler.
jsonrpcConnA := jsonrpc2.NewConn(ctx, jsonrpc2.NewPlainObjectStream(connA), &myHandler{})
defer jsonrpcConnA.Close()
// Send the JSON-RPC notification. // The following JSON-RPC connection has no handler, meaning that it is
go func() { // configured to only be a client. It can send requests and receive the
// Set params to nil. // responses to those requests, but it will ignore any incoming requests.
if err := rpcConn.Notify(ctx, "foo", nil); err != nil { jsonrpcConnB := jsonrpc2.NewConn(ctx, jsonrpc2.NewPlainObjectStream(connB), nil)
fmt.Fprintln(os.Stderr, "notify:", err) defer jsonrpcConnB.Close()
}
}()
// Read the raw JSON-RPC notification on connB. // Send a request from jsonrpcConnB to jsonrpcConnA. The result of a
// // successful call is stored in the result variable.
// Reading the raw JSON-RPC request is for the purpose of this example only. var result string
// Use a jsonrpc2.Handler to read parsed requests. if err := jsonrpcConnB.Call(ctx, "sayHello", nil, &result); err != nil {
buf := make([]byte, 64) fmt.Fprintln(os.Stderr, err)
n, err := connB.Read(buf) return
if err != nil {
fmt.Fprintln(os.Stderr, "read:", err)
} }
fmt.Printf("%s\n", buf[:n]) fmt.Println(result)
// Output: {"jsonrpc":"2.0","method":"foo"} // Output: hello world
} }
// Send a JSON-RPC notification with its params member set to null. // myHandler is the jsonrpc2.Handler used by jsonrpcConnA.
func ExampleConn_Notify_nullParams() { type myHandler struct{}
ctx := context.Background()
connA, connB := net.Pipe() // Handle implements the jsonrpc2.Handler interface.
defer connA.Close() func (h *myHandler) Handle(ctx context.Context, c *jsonrpc2.Conn, r *jsonrpc2.Request) {
defer connB.Close() switch r.Method {
case "sayHello":
rpcConn := jsonrpc2.NewConn(ctx, jsonrpc2.NewPlainObjectStream(connA), nil) if err := c.Reply(ctx, r.ID, "hello world"); err != nil {
log.Println(err)
// Send the JSON-RPC notification. return
go func() { }
// Set params to the JSON null value. default:
params := json.RawMessage("null") err := &jsonrpc2.Error{Code: jsonrpc2.CodeMethodNotFound, Message: "Method not found"}
if err := rpcConn.Notify(ctx, "foo", params); err != nil { if err := c.ReplyWithError(ctx, r.ID, err); err != nil {
fmt.Fprintln(os.Stderr, "notify:", err) log.Println(err)
return
} }
}()
// Read the raw JSON-RPC notification on connB.
//
// Reading the raw JSON-RPC request is for the purpose of this example only.
// Use a jsonrpc2.Handler to read parsed requests.
buf := make([]byte, 64)
n, err := connB.Read(buf)
if err != nil {
fmt.Fprintln(os.Stderr, "read:", err)
} }
fmt.Printf("%s\n", buf[:n])
// Output: {"jsonrpc":"2.0","method":"foo","params":null}
} }

View file

@ -30,20 +30,16 @@ func (h *HandlerWithErrorConfigurer) Handle(ctx context.Context, conn *Conn, req
if err == nil { if err == nil {
err = resp.SetResult(result) err = resp.SetResult(result)
} }
if err != nil {
if e, ok := err.(*Error); ok { if e, ok := err.(*Error); ok {
resp.Error = e resp.Error = e
} else { } else if err != nil {
resp.Error = &Error{Message: err.Error()} resp.Error = &Error{Message: err.Error()}
}
} }
if !req.Notif { err = conn.SendResponse(ctx, resp)
if err := conn.SendResponse(ctx, resp); err != nil { if err != nil && (err != ErrClosed || !h.suppressErrClosed) {
if err != ErrClosed || !h.suppressErrClosed { conn.logger.Printf("jsonrpc2 handler: sending response %s: %v\n", resp.ID, err)
conn.logger.Printf("jsonrpc2 handler: sending response %s: %v\n", resp.ID, err)
}
}
} }
} }

View file

@ -59,10 +59,10 @@ const (
// Handler handles JSON-RPC requests and notifications. // Handler handles JSON-RPC requests and notifications.
type Handler interface { type Handler interface {
// Handle is called to handle a request. No other requests are handled // Handle is called to handle a request. No other requests are handled until
// until it returns. If you do not require strict ordering behavior // it returns. If you do not require strict ordering behavior of received
// of received RPCs, it is suggested to wrap your handler in // RPCs, it is suggested to wrap your handler in AsyncHandler. The context
// AsyncHandler. // is automatically canceled when the connection closes.
Handle(context.Context, *Conn, *Request) Handle(context.Context, *Conn, *Request)
} }

View file

@ -55,6 +55,10 @@ func (r Request) MarshalJSON() ([]byte, error) {
// UnmarshalJSON implements json.Unmarshaler. // UnmarshalJSON implements json.Unmarshaler.
func (r *Request) UnmarshalJSON(data []byte) error { func (r *Request) UnmarshalJSON(data []byte) error {
r2 := make(map[string]interface{}) r2 := make(map[string]interface{})
pop := func(key string) interface{} {
defer delete(r2, key)
return r2[key]
}
// Detect if the "params" or "meta" fields are JSON "null" or just not // Detect if the "params" or "meta" fields are JSON "null" or just not
// present by seeing if the field gets overwritten to nil. // present by seeing if the field gets overwritten to nil.
@ -68,36 +72,37 @@ func (r *Request) UnmarshalJSON(data []byte) error {
if err := decoder.Decode(&r2); err != nil { if err := decoder.Decode(&r2); err != nil {
return err return err
} }
var ok bool var ok bool
r.Method, ok = r2["method"].(string) r.Method, ok = pop("method").(string)
if !ok { if !ok {
return errors.New("missing method field") return errors.New("missing method field")
} }
switch { switch params := pop("params"); params {
case r2["params"] == nil: case nil:
r.Params = &jsonNull r.Params = &jsonNull
case r2["params"] == emptyParams: case emptyParams:
r.Params = nil r.Params = nil
default: default:
b, err := json.Marshal(r2["params"]) b, err := json.Marshal(params)
if err != nil { if err != nil {
return fmt.Errorf("failed to marshal params: %w", err) return fmt.Errorf("failed to marshal params: %w", err)
} }
r.Params = (*json.RawMessage)(&b) r.Params = (*json.RawMessage)(&b)
} }
switch { switch meta := pop("meta"); meta {
case r2["meta"] == nil: case nil:
r.Meta = &jsonNull r.Meta = &jsonNull
case r2["meta"] == emptyMeta: case emptyMeta:
r.Meta = nil r.Meta = nil
default: default:
b, err := json.Marshal(r2["meta"]) b, err := json.Marshal(meta)
if err != nil { if err != nil {
return fmt.Errorf("failed to marshal Meta: %w", err) return fmt.Errorf("failed to marshal Meta: %w", err)
} }
r.Meta = (*json.RawMessage)(&b) r.Meta = (*json.RawMessage)(&b)
} }
switch rawID := r2["id"].(type) { switch rawID := pop("id").(type) {
case nil: case nil:
r.ID = ID{} r.ID = ID{}
r.Notif = true r.Notif = true
@ -115,13 +120,12 @@ func (r *Request) UnmarshalJSON(data []byte) error {
return fmt.Errorf("unexpected ID type: %T", rawID) return fmt.Errorf("unexpected ID type: %T", rawID)
} }
// The jsonrpc field should not be added to ExtraFields.
delete(r2, "jsonrpc")
// Clear the extra fields before populating them again. // Clear the extra fields before populating them again.
r.ExtraFields = nil r.ExtraFields = nil
for name, value := range r2 { for name, value := range r2 {
switch name {
case "id", "jsonrpc", "meta", "method", "params":
continue
}
r.ExtraFields = append(r.ExtraFields, RequestField{ r.ExtraFields = append(r.ExtraFields, RequestField{
Name: name, Name: name,
Value: value, Value: value,