1
0
Fork 0
mirror of https://github.com/sourcegraph/jsonrpc2.git synced 2026-06-16 12:14:56 +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:
branches:
- master
permissions:
contents: read
jobs:
test:
strategy:
@ -14,9 +18,9 @@ jobs:
name: Go ${{ matrix.go }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v6
- name: Set up Go
uses: actions/setup-go@v2
uses: actions/setup-go@v6
with:
go-version: ${{ matrix.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).
This package is **experimental** until further notice.
[**Open the code in Sourcegraph**](https://sourcegraph.com/github.com/sourcegraph/jsonrpc2)
* [Documentation](https://pkg.go.dev/github.com/sourcegraph/jsonrpc2)
* [Open the code in Sourcegraph](https://sourcegraph.com/github.com/sourcegraph/jsonrpc2)
## 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
cancelCtx context.CancelFunc
disconnect chan struct{}
logger Logger
@ -43,13 +44,19 @@ var _ JSONRPC2 = (*Conn)(nil)
// JSON-RPC protocol is symmetric, so a Conn runs on both ends of a
// client-server connection.
//
// NewClient consumes conn, so you should call Close on the returned
// client not on the given conn.
// NewConn consumes stream, so you should call Close on the returned
// 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 {
ctx, cancel := context.WithCancel(ctx)
c := &Conn{
stream: stream,
h: h,
pending: map[ID]*call{},
cancelCtx: cancel,
disconnect: make(chan struct{}),
logger: log.New(os.Stderr, "", log.LstdFlags),
}
@ -60,6 +67,12 @@ func NewConn(ctx context.Context, stream ObjectStream, h Handler, opts ...ConnOp
opt(c)
}
go c.readMessages(ctx)
go func() {
<-ctx.Done()
c.close(nil)
}()
return c
}
@ -166,9 +179,7 @@ func (c *Conn) SendResponse(ctx context.Context, resp *Response) error {
}
func (c *Conn) close(cause error) error {
c.sending.Lock()
c.mu.Lock()
defer c.sending.Unlock()
defer c.mu.Unlock()
if c.closed {
@ -184,20 +195,23 @@ func (c *Conn) close(cause error) error {
}
close(c.disconnect)
c.cancelCtx()
c.closed = true
return c.stream.Close()
}
func (c *Conn) readMessages(ctx context.Context) {
var err error
for err == nil {
for {
var m anyMessage
err = c.stream.ReadObject(&m)
err := c.stream.ReadObject(&m)
if err != nil {
break
c.close(err)
return
}
switch {
// TODO: handle the case where both request and response are nil.
case m.request != nil:
for _, onRecv := range c.onRecv {
onRecv(m.request, nil)
@ -206,49 +220,53 @@ func (c *Conn) readMessages(ctx context.Context) {
case m.response != nil:
resp := m.response
if resp != nil {
id := resp.ID
c.mu.Lock()
call := c.pending[id]
delete(c.pending, id)
c.mu.Unlock()
id := resp.ID
c.mu.Lock()
call := c.pending[id]
delete(c.pending, id)
c.mu.Unlock()
if call != nil {
call.response = resp
}
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)
}
var req *Request
if call != nil {
call.response = resp
req = call.request
}
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) {
c.sending.Lock()
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
// clean up pending
var id ID
@ -330,25 +348,20 @@ type Waiter struct {
// error is returned.
func (w Waiter) Wait(ctx context.Context, result interface{}) error {
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():
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")
}
for i := range msgs {
if err := checkType(&msg{
ID: msgs[i].ID,
Method: msgs[i].Method,
Result: msgs[i].Result,
Error: msgs[i].Error,
}); err != nil {
if err := checkType(&msgs[i]); err != nil {
return err
}
}

View file

@ -43,6 +43,20 @@ func LogMessages(logger Logger) ConnOpt {
OnRecv(func(req *Request, resp *Response) {
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:
mu.Lock()
reqMethods[req.ID] = req.Method
@ -54,34 +68,10 @@ func LogMessages(logger Logger) ConnOpt {
} else {
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)
OnSend(func(req *Request, resp *Response) {
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:
mu.Lock()
method := reqMethods[resp.ID]
@ -98,6 +88,14 @@ func LogMessages(logger Logger) ConnOpt {
err, _ := json.Marshal(resp.Error)
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)
}

View file

@ -51,3 +51,80 @@ func TestSetLogger(t *testing.T) {
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"
)
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 {
sendParams interface{}
wantParams *json.RawMessage
@ -118,38 +170,77 @@ func TestConn_DisconnectNotify(t *testing.T) {
}
func TestConn_Close(t *testing.T) {
t.Run("waiting for response", func(t *testing.T) {
connA, connB := net.Pipe()
nodeA := jsonrpc2.NewConn(
context.Background(),
jsonrpc2.NewPlainObjectStream(connA), noopHandler{},
)
defer nodeA.Close()
nodeB := jsonrpc2.NewConn(
context.Background(),
jsonrpc2.NewPlainObjectStream(connB),
noopHandler{},
)
defer nodeB.Close()
ready := make(chan struct{})
done := make(chan struct{})
go func() {
close(ready)
err := nodeB.Call(context.Background(), "m", nil, nil)
if err != jsonrpc2.ErrClosed {
t.Errorf("got error %v, want %v", err, jsonrpc2.ErrClosed)
cases := []struct {
name string
run func(*testing.T, context.Context, *jsonrpc2.Conn)
}{{
name: "during Call",
run: func(t *testing.T, ctx context.Context, conn *jsonrpc2.Conn) {
ready := make(chan struct{})
done := make(chan struct{})
go func() {
close(ready)
err := conn.Call(ctx, "m", nil, nil)
if err != jsonrpc2.ErrClosed {
t.Errorf("got error %v, want %v", err, jsonrpc2.ErrClosed)
}
close(done)
}()
// Wait for the request to be sent before we close the connection.
<-ready
if err := conn.Close(); err != nil && err != jsonrpc2.ErrClosed {
t.Error(err)
}
close(done)
}()
// Wait for the request to be sent before we close the connection.
<-ready
if err := nodeB.Close(); err != nil && err != jsonrpc2.ErrClosed {
t.Error(err)
}
assertDisconnect(t, nodeB, connB)
<-done
})
<-done
},
}, {
name: "during Wait",
run: func(t *testing.T, ctx context.Context, conn *jsonrpc2.Conn) {
call, err := conn.DispatchCall(ctx, "m", nil, nil)
if err != nil {
t.Fatal(err)
}
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) {
@ -159,12 +250,12 @@ func testParams(t *testing.T, want *json.RawMessage, fn func(c *jsonrpc2.Conn) e
wg.Done()
})
client, server := newClientServer(handler)
defer client.Close()
defer server.Close()
connA, connB := Pipe(context.Background(), noopHandler{}, handler)
defer connA.Close()
defer connB.Close()
wg.Add(1)
if err := fn(client); err != nil {
if err := fn(connA); err != nil {
t.Error(err)
}
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) {
ctx := context.Background()
connA, connB := net.Pipe()
client = jsonrpc2.NewConn(
ctx,
jsonrpc2.NewPlainObjectStream(connA),
noopHandler{},
)
server = jsonrpc2.NewConn(
ctx,
jsonrpc2.NewPlainObjectStream(connB),
handler,
)
return client, server
// Pipe returns two jsonrpc2.Conn, connected via a synchronous, in-memory, full
// duplex network connection.
func Pipe(ctx context.Context, handlerA, handlerB jsonrpc2.Handler) (connA *jsonrpc2.Conn, connB *jsonrpc2.Conn) {
a, b := net.Pipe()
connA = jsonrpc2.NewConn(ctx, jsonrpc2.NewPlainObjectStream(a), handlerA)
connB = jsonrpc2.NewConn(ctx, jsonrpc2.NewPlainObjectStream(b), handlerB)
return connA, connB
}

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 (
"context"
"encoding/json"
"fmt"
"log"
"net"
"os"
"github.com/sourcegraph/jsonrpc2"
)
// Send a JSON-RPC notification with its params member omitted.
func ExampleConn_Notify_paramsOmitted() {
func Example() {
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()
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.
go func() {
// Set params to nil.
if err := rpcConn.Notify(ctx, "foo", nil); err != nil {
fmt.Fprintln(os.Stderr, "notify:", err)
}
}()
// The following JSON-RPC connection has no handler, meaning that it is
// configured to only be a client. It can send requests and receive the
// responses to those requests, but it will ignore any incoming requests.
jsonrpcConnB := jsonrpc2.NewConn(ctx, jsonrpc2.NewPlainObjectStream(connB), nil)
defer jsonrpcConnB.Close()
// 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)
// Send a request from jsonrpcConnB to jsonrpcConnA. The result of a
// successful call is stored in the result variable.
var result string
if err := jsonrpcConnB.Call(ctx, "sayHello", nil, &result); err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
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.
func ExampleConn_Notify_nullParams() {
ctx := context.Background()
// myHandler is the jsonrpc2.Handler used by jsonrpcConnA.
type myHandler struct{}
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)
// Handle implements the jsonrpc2.Handler interface.
func (h *myHandler) Handle(ctx context.Context, c *jsonrpc2.Conn, r *jsonrpc2.Request) {
switch r.Method {
case "sayHello":
if err := c.Reply(ctx, r.ID, "hello world"); err != nil {
log.Println(err)
return
}
default:
err := &jsonrpc2.Error{Code: jsonrpc2.CodeMethodNotFound, Message: "Method not found"}
if err := c.ReplyWithError(ctx, r.ID, err); err != nil {
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 {
err = resp.SetResult(result)
}
if err != nil {
if e, ok := err.(*Error); ok {
resp.Error = e
} else {
resp.Error = &Error{Message: err.Error()}
}
if e, ok := err.(*Error); ok {
resp.Error = e
} else if err != nil {
resp.Error = &Error{Message: err.Error()}
}
if !req.Notif {
if err := conn.SendResponse(ctx, resp); err != nil {
if err != ErrClosed || !h.suppressErrClosed {
conn.logger.Printf("jsonrpc2 handler: sending response %s: %v\n", resp.ID, err)
}
}
err = conn.SendResponse(ctx, resp)
if err != nil && (err != ErrClosed || !h.suppressErrClosed) {
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.
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.
// 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(context.Context, *Conn, *Request)
}

View file

@ -55,6 +55,10 @@ func (r Request) MarshalJSON() ([]byte, error) {
// UnmarshalJSON implements json.Unmarshaler.
func (r *Request) UnmarshalJSON(data []byte) error {
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
// 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 {
return err
}
var ok bool
r.Method, ok = r2["method"].(string)
r.Method, ok = pop("method").(string)
if !ok {
return errors.New("missing method field")
}
switch {
case r2["params"] == nil:
switch params := pop("params"); params {
case nil:
r.Params = &jsonNull
case r2["params"] == emptyParams:
case emptyParams:
r.Params = nil
default:
b, err := json.Marshal(r2["params"])
b, err := json.Marshal(params)
if err != nil {
return fmt.Errorf("failed to marshal params: %w", err)
}
r.Params = (*json.RawMessage)(&b)
}
switch {
case r2["meta"] == nil:
switch meta := pop("meta"); meta {
case nil:
r.Meta = &jsonNull
case r2["meta"] == emptyMeta:
case emptyMeta:
r.Meta = nil
default:
b, err := json.Marshal(r2["meta"])
b, err := json.Marshal(meta)
if err != nil {
return fmt.Errorf("failed to marshal Meta: %w", err)
}
r.Meta = (*json.RawMessage)(&b)
}
switch rawID := r2["id"].(type) {
switch rawID := pop("id").(type) {
case nil:
r.ID = ID{}
r.Notif = true
@ -115,13 +120,12 @@ func (r *Request) UnmarshalJSON(data []byte) error {
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.
r.ExtraFields = nil
for name, value := range r2 {
switch name {
case "id", "jsonrpc", "meta", "method", "params":
continue
}
r.ExtraFields = append(r.ExtraFields, RequestField{
Name: name,
Value: value,