Skip to content

Commit d0c8359

Browse files
committed
concat batch responses manually
1 parent f103674 commit d0c8359

2 files changed

Lines changed: 57 additions & 3 deletions

File tree

jsonrpc/server.go

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -535,9 +535,8 @@ func (s *Server) handleBatchRequest(ctx context.Context, batchReq []json.RawMess
535535
return nil, finalHeaders, nil
536536
}
537537

538-
result, err := json.Marshal(responses)
539-
540-
return result, finalHeaders, err // todo: fix batch request aggregate header
538+
// todo: fix batch request aggregate header
539+
return concatBatchResponses(responses), finalHeaders, nil
541540
}
542541

543542
func isBatch(reader *bufio.Reader) bool {
@@ -568,6 +567,27 @@ func isNilOrEmpty(i any) (bool, error) {
568567
}
569568
}
570569

570+
// concatBatchResponses builds the JSON array from elements that already valid
571+
// JSON, so it joins bytes instead of re-encoding. json.Marshal would run every
572+
// byte through compact() again, which is costly
573+
func concatBatchResponses(responses []json.RawMessage) []byte {
574+
size := len(responses) + 1
575+
for _, response := range responses {
576+
size += len(response)
577+
}
578+
579+
result := make([]byte, 0, size)
580+
result = append(result, '[')
581+
for i, response := range responses {
582+
if i > 0 {
583+
result = append(result, ',')
584+
}
585+
result = append(result, response...)
586+
}
587+
588+
return append(result, ']')
589+
}
590+
571591
// TODO: add recover() to catch panics from handlers/validators and return a JSON-RPC internal error
572592
// instead of crashing the HTTP connection
573593
func (s *Server) handleRequest(ctx context.Context, req *Request) (*response, http.Header, error) {

jsonrpc/server_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1051,3 +1051,37 @@ func TestBatchResponseSizeLimit(t *testing.T) {
10511051
}
10521052
})
10531053
}
1054+
1055+
func TestBatchArrayIsByteIdenticalToJSONMarshal(t *testing.T) {
1056+
server := jsonrpc.NewServer(1, log.NewNopZapLogger())
1057+
require.NoError(t, server.RegisterMethods(jsonrpc.Method{
1058+
Name: "echo",
1059+
Params: []jsonrpc.Parameter{{Name: "data"}},
1060+
Handler: func(data string) (string, *jsonrpc.Error) { return data, nil },
1061+
}))
1062+
1063+
payloads := []string{
1064+
"<script>a && b</script>",
1065+
"line\u2028sep\u2029par",
1066+
`quote " backslash \`,
1067+
"",
1068+
}
1069+
1070+
elements := make([]string, len(payloads))
1071+
for i, payload := range payloads {
1072+
params, err := json.Marshal([]string{payload})
1073+
require.NoError(t, err)
1074+
elements[i] = fmt.Sprintf(`{"jsonrpc":"2.0","id":%d,"method":"echo","params":%s}`, i+1, params)
1075+
}
1076+
1077+
body, _, err := server.HandleReader(t.Context(), strings.NewReader("["+strings.Join(elements, ",")+"]"))
1078+
require.NoError(t, err)
1079+
1080+
var got []json.RawMessage
1081+
require.NoError(t, json.Unmarshal(body, &got))
1082+
require.Len(t, got, len(payloads))
1083+
1084+
want, err := json.Marshal(got)
1085+
require.NoError(t, err)
1086+
assert.Equal(t, string(want), string(body))
1087+
}

0 commit comments

Comments
 (0)