1
0
Fork 0
mirror of https://github.com/sourcegraph/jsonrpc2.git synced 2026-06-17 04:30:04 +02:00

Compare commits

..

No commits in common. "master" and "v0.2.0" have entirely different histories.

15 changed files with 233 additions and 496 deletions

View file

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

View file

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

13
.github/workflows/lsif.yml vendored Normal file
View file

@ -0,0 +1,13 @@
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 }}

View file

@ -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@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,8 +3,9 @@
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).
* [Documentation](https://pkg.go.dev/github.com/sourcegraph/jsonrpc2) This package is **experimental** until further notice.
* [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

View file

@ -1,12 +0,0 @@
# 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.

128
conn.go
View file

@ -27,7 +27,6 @@ type Conn struct {
sending sync.Mutex sending sync.Mutex
cancelCtx context.CancelFunc
disconnect chan struct{} disconnect chan struct{}
logger Logger logger Logger
@ -44,19 +43,13 @@ 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.
// //
// NewConn consumes stream, so you should call Close on the returned // NewClient consumes conn, so you should call Close on the returned
// Conn not on the given stream or its underlying connection. // client not on the given conn.
//
// 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),
} }
@ -67,12 +60,6 @@ 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
} }
@ -179,7 +166,9 @@ 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 {
@ -195,23 +184,20 @@ 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) {
for { var err error
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 {
c.close(err) break
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)
@ -220,53 +206,49 @@ func (c *Conn) readMessages(ctx context.Context) {
case m.response != nil: case m.response != nil:
resp := m.response resp := m.response
id := resp.ID if resp != nil {
c.mu.Lock() id := resp.ID
call := c.pending[id] c.mu.Lock()
delete(c.pending, id) call := c.pending[id]
c.mu.Unlock() delete(c.pending, id)
c.mu.Unlock()
var req *Request if call != nil {
if call != nil { call.response = resp
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
@ -348,20 +330,25 @@ 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 <-ctx.Done():
return ctx.Err()
case err, ok := <-w.call.done: case err, ok := <-w.call.done:
if !ok { if !ok {
return ErrClosed err = ErrClosed
} }
if err != nil || result == nil { if err != nil {
return err return err
} }
if w.call.response.Result == nil { if result != nil {
w.call.response.Result = &jsonNull 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 json.Unmarshal(*w.call.response.Result, result) return nil
case <-ctx.Done():
return ctx.Err()
} }
} }
@ -427,7 +414,12 @@ 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(&msgs[i]); err != nil { if err := checkType(&msg{
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,20 +43,6 @@ 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
@ -68,10 +54,34 @@ 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]
@ -88,14 +98,6 @@ 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,80 +51,3 @@ 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,58 +14,6 @@ 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
@ -170,77 +118,38 @@ func TestConn_DisconnectNotify(t *testing.T) {
} }
func TestConn_Close(t *testing.T) { func TestConn_Close(t *testing.T) {
cases := []struct { t.Run("waiting for response", func(t *testing.T) {
name string connA, connB := net.Pipe()
run func(*testing.T, context.Context, *jsonrpc2.Conn) nodeA := jsonrpc2.NewConn(
}{{ context.Background(),
name: "during Call", jsonrpc2.NewPlainObjectStream(connA), noopHandler{},
run: func(t *testing.T, ctx context.Context, conn *jsonrpc2.Conn) { )
ready := make(chan struct{}) defer nodeA.Close()
done := make(chan struct{}) nodeB := jsonrpc2.NewConn(
go func() { context.Background(),
close(ready) jsonrpc2.NewPlainObjectStream(connB),
err := conn.Call(ctx, "m", nil, nil) noopHandler{},
if err != jsonrpc2.ErrClosed { )
t.Errorf("got error %v, want %v", err, jsonrpc2.ErrClosed) defer nodeB.Close()
}
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)
}
<-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() ready := make(chan struct{})
nodeA := jsonrpc2.NewConn( done := make(chan struct{})
ctx, go func() {
jsonrpc2.NewPlainObjectStream(connA), noopHandler{}, close(ready)
) err := nodeB.Call(context.Background(), "m", nil, nil)
defer nodeA.Close() if err != jsonrpc2.ErrClosed {
nodeB := jsonrpc2.NewConn( t.Errorf("got error %v, want %v", err, jsonrpc2.ErrClosed)
ctx, }
jsonrpc2.NewPlainObjectStream(connB), close(done)
noopHandler{}, }()
) // Wait for the request to be sent before we close the connection.
defer nodeB.Close() <-ready
if err := nodeB.Close(); err != nil && err != jsonrpc2.ErrClosed {
tc.run(t, ctx, nodeB) t.Error(err)
}
assertDisconnect(t, nodeB, connB) assertDisconnect(t, nodeB, connB)
}) <-done
} })
} }
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) {
@ -250,12 +159,12 @@ func testParams(t *testing.T, want *json.RawMessage, fn func(c *jsonrpc2.Conn) e
wg.Done() wg.Done()
}) })
connA, connB := Pipe(context.Background(), noopHandler{}, handler) client, server := newClientServer(handler)
defer connA.Close() defer client.Close()
defer connB.Close() defer server.Close()
wg.Add(1) wg.Add(1)
if err := fn(connA); err != nil { if err := fn(client); err != nil {
t.Error(err) t.Error(err)
} }
wg.Wait() wg.Wait()
@ -294,11 +203,18 @@ func assertRawJSONMessage(t *testing.T, got *json.RawMessage, want *json.RawMess
} }
} }
// Pipe returns two jsonrpc2.Conn, connected via a synchronous, in-memory, full func newClientServer(handler jsonrpc2.Handler) (client *jsonrpc2.Conn, server *jsonrpc2.Conn) {
// duplex network connection. ctx := context.Background()
func Pipe(ctx context.Context, handlerA, handlerB jsonrpc2.Handler) (connA *jsonrpc2.Conn, connB *jsonrpc2.Conn) { connA, connB := net.Pipe()
a, b := net.Pipe() client = jsonrpc2.NewConn(
connA = jsonrpc2.NewConn(ctx, jsonrpc2.NewPlainObjectStream(a), handlerA) ctx,
connB = jsonrpc2.NewConn(ctx, jsonrpc2.NewPlainObjectStream(b), handlerB) jsonrpc2.NewPlainObjectStream(connA),
return connA, connB noopHandler{},
)
server = jsonrpc2.NewConn(
ctx,
jsonrpc2.NewPlainObjectStream(connB),
handler,
)
return client, server
} }

View file

@ -1,78 +0,0 @@
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,63 +2,77 @@ package jsonrpc2_test
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"log"
"net" "net"
"os" "os"
"github.com/sourcegraph/jsonrpc2" "github.com/sourcegraph/jsonrpc2"
) )
func Example() { // Send a JSON-RPC notification with its params member omitted.
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()
// The following JSON-RPC connection is both a client and a server. It can rpcConn := jsonrpc2.NewConn(ctx, jsonrpc2.NewPlainObjectStream(connA), nil)
// 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()
// The following JSON-RPC connection has no handler, meaning that it is // Send the JSON-RPC notification.
// configured to only be a client. It can send requests and receive the go func() {
// responses to those requests, but it will ignore any incoming requests. // Set params to nil.
jsonrpcConnB := jsonrpc2.NewConn(ctx, jsonrpc2.NewPlainObjectStream(connB), nil) if err := rpcConn.Notify(ctx, "foo", nil); err != nil {
defer jsonrpcConnB.Close() fmt.Fprintln(os.Stderr, "notify:", err)
}
}()
// Send a request from jsonrpcConnB to jsonrpcConnA. The result of a // Read the raw JSON-RPC notification on connB.
// successful call is stored in the result variable. //
var result string // Reading the raw JSON-RPC request is for the purpose of this example only.
if err := jsonrpcConnB.Call(ctx, "sayHello", nil, &result); err != nil { // Use a jsonrpc2.Handler to read parsed requests.
fmt.Fprintln(os.Stderr, err) buf := make([]byte, 64)
return n, err := connB.Read(buf)
if err != nil {
fmt.Fprintln(os.Stderr, "read:", err)
} }
fmt.Println(result) fmt.Printf("%s\n", buf[:n])
// Output: hello world // Output: {"jsonrpc":"2.0","method":"foo"}
} }
// myHandler is the jsonrpc2.Handler used by jsonrpcConnA. // Send a JSON-RPC notification with its params member set to null.
type myHandler struct{} func ExampleConn_Notify_nullParams() {
ctx := context.Background()
// Handle implements the jsonrpc2.Handler interface. connA, connB := net.Pipe()
func (h *myHandler) Handle(ctx context.Context, c *jsonrpc2.Conn, r *jsonrpc2.Request) { defer connA.Close()
switch r.Method { defer connB.Close()
case "sayHello":
if err := c.Reply(ctx, r.ID, "hello world"); err != nil { rpcConn := jsonrpc2.NewConn(ctx, jsonrpc2.NewPlainObjectStream(connA), nil)
log.Println(err)
return // Send the JSON-RPC notification.
} go func() {
default: // Set params to the JSON null value.
err := &jsonrpc2.Error{Code: jsonrpc2.CodeMethodNotFound, Message: "Method not found"} params := json.RawMessage("null")
if err := c.ReplyWithError(ctx, r.ID, err); err != nil { if err := rpcConn.Notify(ctx, "foo", params); err != nil {
log.Println(err) fmt.Fprintln(os.Stderr, "notify:", 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,16 +30,20 @@ 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 if err != nil { } else {
resp.Error = &Error{Message: err.Error()} resp.Error = &Error{Message: err.Error()}
}
} }
err = conn.SendResponse(ctx, resp) if !req.Notif {
if err != nil && (err != ErrClosed || !h.suppressErrClosed) { if err := conn.SendResponse(ctx, resp); err != nil {
conn.logger.Printf("jsonrpc2 handler: sending response %s: %v\n", resp.ID, err) if 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. // 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 until // Handle is called to handle a request. No other requests are handled
// it returns. If you do not require strict ordering behavior of received // until it returns. If you do not require strict ordering behavior
// RPCs, it is suggested to wrap your handler in AsyncHandler. The context // of received RPCs, it is suggested to wrap your handler in
// is automatically canceled when the connection closes. // AsyncHandler.
Handle(context.Context, *Conn, *Request) Handle(context.Context, *Conn, *Request)
} }

View file

@ -55,10 +55,6 @@ 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.
@ -72,37 +68,36 @@ 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 = pop("method").(string) r.Method, ok = r2["method"].(string)
if !ok { if !ok {
return errors.New("missing method field") return errors.New("missing method field")
} }
switch params := pop("params"); params { switch {
case nil: case r2["params"] == nil:
r.Params = &jsonNull r.Params = &jsonNull
case emptyParams: case r2["params"] == emptyParams:
r.Params = nil r.Params = nil
default: default:
b, err := json.Marshal(params) b, err := json.Marshal(r2["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 meta := pop("meta"); meta { switch {
case nil: case r2["meta"] == nil:
r.Meta = &jsonNull r.Meta = &jsonNull
case emptyMeta: case r2["meta"] == emptyMeta:
r.Meta = nil r.Meta = nil
default: default:
b, err := json.Marshal(meta) b, err := json.Marshal(r2["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 := pop("id").(type) { switch rawID := r2["id"].(type) {
case nil: case nil:
r.ID = ID{} r.ID = ID{}
r.Notif = true r.Notif = true
@ -120,12 +115,13 @@ 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,