diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 2390d8c..0000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,10 +0,0 @@ -version: 2 -updates: - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "monthly" - groups: - github-actions: - patterns: - - "*" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 29413b6..9bf8e75 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,10 +4,6 @@ on: push: branches: - master - -permissions: - contents: read - jobs: test: strategy: @@ -18,9 +14,9 @@ jobs: name: Go ${{ matrix.go }} runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v3 - name: Set up Go - uses: actions/setup-go@v6 + uses: actions/setup-go@v2 with: go-version: ${{ matrix.go }} id: go diff --git a/.github/workflows/lsif.yml b/.github/workflows/lsif.yml new file mode 100644 index 0000000..83d4bfd --- /dev/null +++ b/.github/workflows/lsif.yml @@ -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 }} diff --git a/.github/workflows/scip.yml b/.github/workflows/scip.yml deleted file mode 100644 index c0dc33b..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@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 }} diff --git a/README.md b/README.md index df4f081..d2406ab 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,9 @@ 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) -* [Open the code in Sourcegraph](https://sourcegraph.com/github.com/sourcegraph/jsonrpc2) +This package is **experimental** until further notice. + +[**Open the code in Sourcegraph**](https://sourcegraph.com/github.com/sourcegraph/jsonrpc2) ## Known issues diff --git a/SECURITY.md b/SECURITY.md deleted file mode 100644 index d889a1f..0000000 --- a/SECURITY.md +++ /dev/null @@ -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. - diff --git a/conn.go b/conn.go index 35c6279..9de994a 100644 --- a/conn.go +++ b/conn.go @@ -27,7 +27,6 @@ type Conn struct { sending sync.Mutex - cancelCtx context.CancelFunc disconnect chan struct{} logger Logger @@ -44,19 +43,13 @@ var _ JSONRPC2 = (*Conn)(nil) // JSON-RPC protocol is symmetric, so a Conn runs on both ends of a // client-server connection. // -// 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. +// NewClient consumes conn, so you should call Close on the returned +// client not on the given conn. 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), } @@ -67,12 +60,6 @@ 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 } @@ -179,7 +166,9 @@ 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 { @@ -195,23 +184,20 @@ 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) { - for { + var err error + for err == nil { var m anyMessage - err := c.stream.ReadObject(&m) + err = c.stream.ReadObject(&m) if err != nil { - c.close(err) - return + break } 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) @@ -220,53 +206,49 @@ func (c *Conn) readMessages(ctx context.Context) { case m.response != nil: resp := m.response - id := resp.ID - c.mu.Lock() - call := c.pending[id] - delete(c.pending, id) - c.mu.Unlock() + if resp != nil { + id := resp.ID + c.mu.Lock() + call := c.pending[id] + delete(c.pending, id) + c.mu.Unlock() - var req *Request - if call != nil { - call.response = resp - req = call.request + 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) + } } - - 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 @@ -348,20 +330,25 @@ type Waiter struct { // error is returned. func (w Waiter) Wait(ctx context.Context, result interface{}) error { select { - case <-ctx.Done(): - return ctx.Err() - case err, ok := <-w.call.done: if !ok { - return ErrClosed + err = ErrClosed } - if err != nil || result == nil { + if err != nil { return err } - if w.call.response.Result == nil { - w.call.response.Result = &jsonNull + 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 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") } 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 } } diff --git a/conn_opt.go b/conn_opt.go index 8a29f80..423cf80 100644 --- a/conn_opt.go +++ b/conn_opt.go @@ -43,20 +43,6 @@ 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 @@ -68,10 +54,34 @@ 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] @@ -88,14 +98,6 @@ 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) } diff --git a/conn_opt_test.go b/conn_opt_test.go index 97f59e4..df53a1a 100644 --- a/conn_opt_test.go +++ b/conn_opt_test.go @@ -51,80 +51,3 @@ 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) - } - } -} diff --git a/conn_test.go b/conn_test.go index 5d2a7e4..56e0350 100644 --- a/conn_test.go +++ b/conn_test.go @@ -14,58 +14,6 @@ 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 @@ -170,77 +118,38 @@ func TestConn_DisconnectNotify(t *testing.T) { } func TestConn_Close(t *testing.T) { - 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) - } - <-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() + 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() - 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) - }) - } + 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) + } + 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 + }) } 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() }) - connA, connB := Pipe(context.Background(), noopHandler{}, handler) - defer connA.Close() - defer connB.Close() + client, server := newClientServer(handler) + defer client.Close() + defer server.Close() wg.Add(1) - if err := fn(connA); err != nil { + if err := fn(client); err != nil { t.Error(err) } 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 -// 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 +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 } diff --git a/example_params_test.go b/example_params_test.go deleted file mode 100644 index 9b2b75a..0000000 --- a/example_params_test.go +++ /dev/null @@ -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} -} diff --git a/example_test.go b/example_test.go index 00f4a57..9b2b75a 100644 --- a/example_test.go +++ b/example_test.go @@ -2,63 +2,77 @@ package jsonrpc2_test import ( "context" + "encoding/json" "fmt" - "log" "net" "os" "github.com/sourcegraph/jsonrpc2" ) -func Example() { +// Send a JSON-RPC notification with its params member omitted. +func ExampleConn_Notify_paramsOmitted() { 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() - // 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() + rpcConn := jsonrpc2.NewConn(ctx, jsonrpc2.NewPlainObjectStream(connA), nil) - // 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() + // 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) + } + }() - // 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 + // 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.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. -type myHandler struct{} +// Send a JSON-RPC notification with its params member set to null. +func ExampleConn_Notify_nullParams() { + ctx := context.Background() -// 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 + 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} } diff --git a/handler_with_error.go b/handler_with_error.go index d727237..2bd5c1d 100644 --- a/handler_with_error.go +++ b/handler_with_error.go @@ -30,16 +30,20 @@ func (h *HandlerWithErrorConfigurer) Handle(ctx context.Context, conn *Conn, req if err == nil { err = resp.SetResult(result) } - - if e, ok := err.(*Error); ok { - resp.Error = e - } else if err != nil { - resp.Error = &Error{Message: err.Error()} + if err != nil { + if e, ok := err.(*Error); ok { + resp.Error = e + } else { + resp.Error = &Error{Message: err.Error()} + } } - 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) + 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) + } + } } } diff --git a/jsonrpc2.go b/jsonrpc2.go index 7d3e132..97e26d7 100644 --- a/jsonrpc2.go +++ b/jsonrpc2.go @@ -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. The context - // is automatically canceled when the connection closes. + // 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(context.Context, *Conn, *Request) } diff --git a/request.go b/request.go index b9cdde0..372b3e7 100644 --- a/request.go +++ b/request.go @@ -55,10 +55,6 @@ 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. @@ -72,37 +68,36 @@ func (r *Request) UnmarshalJSON(data []byte) error { if err := decoder.Decode(&r2); err != nil { return err } - var ok bool - r.Method, ok = pop("method").(string) + r.Method, ok = r2["method"].(string) if !ok { return errors.New("missing method field") } - switch params := pop("params"); params { - case nil: + switch { + case r2["params"] == nil: r.Params = &jsonNull - case emptyParams: + case r2["params"] == emptyParams: r.Params = nil default: - b, err := json.Marshal(params) + b, err := json.Marshal(r2["params"]) if err != nil { return fmt.Errorf("failed to marshal params: %w", err) } r.Params = (*json.RawMessage)(&b) } - switch meta := pop("meta"); meta { - case nil: + switch { + case r2["meta"] == nil: r.Meta = &jsonNull - case emptyMeta: + case r2["meta"] == emptyMeta: r.Meta = nil default: - b, err := json.Marshal(meta) + b, err := json.Marshal(r2["meta"]) if err != nil { return fmt.Errorf("failed to marshal Meta: %w", err) } r.Meta = (*json.RawMessage)(&b) } - switch rawID := pop("id").(type) { + switch rawID := r2["id"].(type) { case nil: r.ID = ID{} r.Notif = true @@ -120,12 +115,13 @@ 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,