Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions common/json_rpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,10 @@ func (r *JsonRpcResponse) ParseFromStream(ctx []context.Context, reader io.Reade
}
}

if len(temp.Error) > 0 {
// Bitcoin-family nodes often emit `"error": null` on success. Treat that
// (and missing error) as no error — ParseError("null") used to invent a
// server-side exception and fail the whole upstream attempt.
if len(temp.Error) > 0 && string(temp.Error) != "null" {
if err := r.ParseError(string(temp.Error)); err != nil {
return err
}
Expand Down Expand Up @@ -399,11 +402,17 @@ func (r *JsonRpcResponse) ParseError(raw string) error {

r.errBytes = nil

// JSON-RPC allows "error": null (Bitcoin Core / dogecoind / litecoind).
// That means success — do not fabricate a server-side exception.
if raw == "null" {
return nil
}

// First attempt to unmarshal the error as a typical JSON-RPC error
var rpcErr ErrJsonRpcExceptionExternal
if err := SonicCfg.UnmarshalFromString(raw, &rpcErr); err != nil {
// Special case: check for non-standard error structures in the raw data
if raw == "" || raw == "null" {
if raw == "" {
r.Error = NewErrJsonRpcExceptionExternal(
int(JsonRpcErrorServerSideException),
"unexpected empty response from upstream endpoint",
Expand Down Expand Up @@ -1205,7 +1214,10 @@ type JsonRpcRequest struct {
JSONRPC string `json:"jsonrpc,omitempty"`
ID interface{} `json:"id,omitempty"`
Method string `json:"method"`
Params []interface{} `json:"params"`
// omitempty: Stellar (and some other non-EVM) reject "params":[] — they
// expect the field absent (or an object). Empty/nil params are omitted on
// the wire; EVM nodes accept both forms.
Params []interface{} `json:"params,omitempty"`

// idRaw stores the verbatim bytes of the id as received from the client.
// This is used to round-trip the id back without precision loss for ids
Expand Down
29 changes: 28 additions & 1 deletion common/json_rpc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,10 +219,23 @@ func TestJsonRpcRequest_MarshalParams(t *testing.T) {
})
assert.NoError(t, err)

expectedRawReq := `{"jsonrpc":"2.0","id":1,"method":"eth_blockNumber","params":[]}`
// Empty params omitted — required for Stellar / some non-EVM nodes
// that reject "params":[] (expect absent field or object).
expectedRawReq := `{"jsonrpc":"2.0","id":1,"method":"eth_blockNumber"}`
assert.Equal(t, expectedRawReq, string(rawReq))
})

t.Run("Nil", func(t *testing.T) {
rawReq, err := SonicCfg.Marshal(JsonRpcRequest{
JSONRPC: "2.0",
ID: 1,
Method: "getLatestLedger",
Params: nil,
})
assert.NoError(t, err)
assert.Equal(t, `{"jsonrpc":"2.0","id":1,"method":"getLatestLedger"}`, string(rawReq))
})

t.Run("Value", func(t *testing.T) {
rawReq, err := SonicCfg.Marshal(JsonRpcRequest{
JSONRPC: "2.0",
Expand All @@ -237,6 +250,20 @@ func TestJsonRpcRequest_MarshalParams(t *testing.T) {
})
}

func TestJsonRpcResponse_ErrorNullIsSuccess(t *testing.T) {
// Bitcoin-family (dogecoind/litecoind) returns "error":null on success.
raw := `{"result":67851510,"error":null,"id":1}`
r := &JsonRpcResponse{}
err := r.ParseFromStream(nil, bytes.NewReader([]byte(raw)), len(raw))
require.NoError(t, err)
assert.Nil(t, r.Error)
assert.Equal(t, "67851510", string(r.result))

r2 := &JsonRpcResponse{}
require.NoError(t, r2.ParseError("null"))
assert.Nil(t, r2.Error)
}

func TestJsonRpcResponse_CanonicalHash_EmptyishNormalization(t *testing.T) {
// Test cases that should produce the same hash due to emptyish normalization
testGroups := []struct {
Expand Down
Loading