jsonrpc2: do not panic when sending unmarshalable params

If params given to (*Connection).Call is not JSON-marshalable, Call
returns immediately an *AsyncCall instance containing the
corresponding error, but its ctx field is not initialized. This
produces a panic later in golang.org/x/exp/event.New, as it does not
expect a nil context.Context.

Fixes golang/go#61654.

Change-Id: I6464ab5fde85670267b25821e0b57af04c331c8c
Reviewed-on: https://go-review.googlesource.com/c/exp/+/516556
Reviewed-by: Bryan Mills <bcmills@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Michael Knyszek <mknyszek@google.com>
Run-TryBot: Bryan Mills <bcmills@google.com>
Auto-Submit: Bryan Mills <bcmills@google.com>
diff --git a/jsonrpc2/conn.go b/jsonrpc2/conn.go
index 882d15b..f0e527c 100644
--- a/jsonrpc2/conn.go
+++ b/jsonrpc2/conn.go
@@ -144,18 +144,18 @@
 		id:        Int64ID(atomic.AddInt64(&c.seq, 1)),
 		resultBox: make(chan asyncResult, 1),
 	}
-	// generate a new request identifier
-	call, err := NewCall(result.id, method, params)
-	if err != nil {
-		//set the result to failed
-		result.resultBox <- asyncResult{err: errors.Errorf("marshaling call parameters: %w", err)}
-		return result
-	}
-	//TODO: rewrite this using the new target/prototype stuff
+	// TODO: rewrite this using the new target/prototype stuff
 	ctx = event.Start(ctx, method,
 		Method(method), RPCDirection(Outbound), RPCID(fmt.Sprintf("%q", result.id)))
 	Started.Record(ctx, 1, Method(method))
 	result.ctx = ctx
+	// generate a new request identifier
+	call, err := NewCall(result.id, method, params)
+	if err != nil {
+		// set the result to failed
+		result.resultBox <- asyncResult{err: errors.Errorf("marshaling call parameters: %w", err)}
+		return result
+	}
 	// We have to add ourselves to the pending map before we send, otherwise we
 	// are racing the response.
 	// rchan is buffered in case the response arrives without a listener.
@@ -355,7 +355,7 @@
 			select {
 			case nextReq, ok = <-fromRead:
 			case toDeliver <- q[0]:
-				//TODO: this causes a lot of shuffling, should we use a growing ring buffer? compaction?
+				// TODO: this causes a lot of shuffling, should we use a growing ring buffer? compaction?
 				q = q[1:]
 			}
 		}
@@ -413,7 +413,7 @@
 	}
 	if err := c.respond(entry, result, rerr); err != nil {
 		// no way to propagate this error
-		//TODO: should we do more than just log it?
+		// TODO: should we do more than just log it?
 		event.Error(entry.baseCtx, "jsonrpc2 message delivery failed", err)
 	}
 }
@@ -441,7 +441,7 @@
 			err = errors.Errorf("%w: %q notification failed: %v", ErrInternal, entry.request.Method, rerr)
 			rerr = nil
 		case result != nil:
-			//notification produced a response, which is an error
+			// notification produced a response, which is an error
 			err = errors.Errorf("%w: %q produced unwanted response", ErrInternal, entry.request.Method)
 		default:
 			// normal notification finish
diff --git a/jsonrpc2/jsonrpc2_test.go b/jsonrpc2/jsonrpc2_test.go
index 85a4981..40df6ed 100644
--- a/jsonrpc2/jsonrpc2_test.go
+++ b/jsonrpc2/jsonrpc2_test.go
@@ -10,6 +10,7 @@
 	"fmt"
 	"path"
 	"reflect"
+	"strings"
 	"testing"
 	"time"
 
@@ -60,6 +61,7 @@
 		notify{"unblock", "a"},
 		collect{"a", true, false},
 	}},
+	callErr{"error", func() {}, "marshaling call parameters: json: unsupported type"},
 }
 
 type binder struct {
@@ -90,6 +92,12 @@
 	expect interface{}
 }
 
+type callErr struct {
+	method    string
+	params    interface{}
+	expectErr string
+}
+
 type async struct {
 	name   string
 	method string
@@ -175,6 +183,18 @@
 	verifyResults(t, test.method, results, test.expect)
 }
 
+func (test callErr) Name() string { return test.method }
+func (test callErr) Invoke(t *testing.T, ctx context.Context, h *handler) {
+	var results interface{}
+	if err := h.conn.Call(ctx, test.method, test.params).Await(ctx, &results); err != nil {
+		if serr := err.Error(); !strings.Contains(serr, test.expectErr) {
+			t.Fatalf("%v:Call failed but with unexpected error: %q does not contain %q", test.method, serr, test.expectErr)
+		}
+		return
+	}
+	t.Fatalf("%v:Call succeeded (%v) but should have failed with error containing %q", test.method, results, test.expectErr)
+}
+
 func (test echo) Invoke(t *testing.T, ctx context.Context, h *handler) {
 	results := newResults(test.expect)
 	if err := h.conn.Call(ctx, "echo", []interface{}{test.method, test.params}).Await(ctx, results); err != nil {